[Erlang 0127] Term sharing in Erlang/OTP 上篇

之前,在 [Erlang 0126] 我们读过的Erlang论文 提到过下面这篇论文:

On Preserving Term Sharing in the Erlang Virtual Machine
地址: http://user.it.uu.se/~kostis/Papers/erlang12_sharing.pdf 
摘要:In this paper we describe our experiences and argue through examples why ?attening terms during copying is not a good idea for
a language like Erlang. More importantly, we propose a sharing preserving copying mechanism for Erlang/OTP and describe a pub-
licly available complete implementation of this mechanism.

Term Sharing  数据项共享这个东西不新鲜,"Efficiency Guide User‘s Guide"的4.2章节"Constructing binaries"[ 链接 ] 和 8.2 章节的"Loss of sharing" [链接]就提到过(再次吐槽一下Erlang文档组织,一个主题往往分散在多个文档里面,需要耐心).不仅仅是Binary数据类型,Term Sharing 是一个通用的问题.在Guide中提到了强制拷贝的场景:发送到别的进程或者插入到ETS. 下面是文档摘抄的:

Loss of sharing
  
  Shared sub-terms are not preserved when a term is sent to another process, passed as the initial process arguments in the spawn call, or stored in an ETS table. That is an optimization. Most applications do not send messages with shared sub-terms.

数据拷贝过程,Erlang会进行两次遍历.第一次遍历,会计算flat size(erts/emulator/beam/copy.c中的size_object 方法)然后为此分配对应的内存,第二次遍历完成实际的拷贝 (function copy_structin erts/emulator/beam/copy.c).

首先,我们写一个简单的代码演示一下后面要频繁用到的erts_debug:size/1和erts_debug:flat_size/1

s3(L)->
    L2=[L,L,L,L],
    {{erts_debug:size(L),erts_debug:flat_size(L)},
      {erts_debug:size(L2),erts_debug:flat_size(L2)}}
.

9> d:s3([1,2,3,4,5,6]).
{{12,12},{20,56}}

  下面在shell这段代码,分别演示了spawn,消息发送,以及插入ETS.

Eshell V6.0  (abort with ^G)
1> L=[1,2,3,4,5,6,7,8,9,10].
[1,2,3,4,5,6,7,8,9,10]
2>  L2=[L,L,L,L,L,L].
[[1,2,3,4,5,6,7,8,9,10],
[1,2,3,4,5,6,7,8,9,10],
[1,2,3,4,5,6,7,8,9,10],
[1,2,3,4,5,6,7,8,9,10],
[1,2,3,4,5,6,7,8,9,10],
[1,2,3,4,5,6,7,8,9,10]]
3>  erts_debug:size(L2).
32
4>  erts_debug:flat_size(L2).
132
5>  spawn(fun () ->receive Data ->  io:format("~p",[erts_debug:size(Data)]) end end).
<0.39.0>
6> v(5) ! L2.
132[[1,2,3,4,5,6,7,8,9,10],
[1,2,3,4,5,6,7,8,9,10],
[1,2,3,4,5,6,7,8,9,10],
[1,2,3,4,5,6,7,8,9,10],
[1,2,3,4,5,6,7,8,9,10],
[1,2,3,4,5,6,7,8,9,10]]
7>  erts_debug:size(L2).
32
8> ets:new(test,[named_table]).
test
9> ets:insert(test,{1,L2}).
true
10>  ets:lookup(test ,1).
[{1,
  [[1,2,3,4,5,6,7,8,9,10],
   [1,2,3,4,5,6,7,8,9,10],
   [1,2,3,4,5,6,7,8,9,10],
   [1,2,3,4,5,6,7,8,9,10],
   [1,2,3,4,5,6,7,8,9,10],
   [1,2,3,4,5,6,7,8,9,10]]}]
11> [{1,Data}]=v(10).
[{1,
  [[1,2,3,4,5,6,7,8,9,10],
   [1,2,3,4,5,6,7,8,9,10],
   [1,2,3,4,5,6,7,8,9,10],
   [1,2,3,4,5,6,7,8,9,10],
   [1,2,3,4,5,6,7,8,9,10],
   [1,2,3,4,5,6,7,8,9,10]]}]
12> Data.
[[1,2,3,4,5,6,7,8,9,10],
[1,2,3,4,5,6,7,8,9,10],
[1,2,3,4,5,6,7,8,9,10],
[1,2,3,4,5,6,7,8,9,10],
[1,2,3,4,5,6,7,8,9,10],
[1,2,3,4,5,6,7,8,9,10]]
13>  erts_debug:size(Data).
132
14> spawn(d,test,[L2]).
132<0.54.0>

 test(Data)->
   io:format("~p",[erts_debug:size(Data)]).

  除了上面的情况,还有一些潜在的情况也会导致数据展开,比如上面提到的论文里面设计的例子:

show_printing_may_be_bad() ->
  F = fun (N) ->
  T = now(),
  L = mklist(N),
  S = erts_debug:size(L),
  io:format("mklist(~w), size ~w, ", [N, S]),
  io:format("is ~P, ", [L, 2]), %%% BAD !!!
  D = timer:now_diff(now(), T),
  io:format("in ~.3f sec.~n", [D/1000000])
   end,
   lists:foreach(F, [10, 20, 22, 24, 26, 28, 30]).

mklist(0) -> 0;
mklist(M) -> X = mklist(M-1), [X, X].

  io:format("is ~P, ", [L, 2]), %%% BAD !!!这行代码删掉前后分别执行代码,在过机器上得到的结果如下:

Eshell V6.0  (abort with ^G)
1>  d:show_printing_may_be_bad().
mklist(10), size 40, in 0.001 sec.
mklist(20), size 80, in 0.000 sec.
mklist(22), size 88, in 0.000 sec.
mklist(24), size 96, in 0.000 sec.
mklist(26), size 104, in 0.000 sec.
mklist(28), size 112, in 0.000 sec.
mklist(30), size 120, in 0.000 sec.
ok

Eshell V6.0  (abort with ^G)
1>  d:show_printing_may_be_bad().
mklist(10), size 40, is [[...]|...], in 0.001 sec.
mklist(20), size 80, is [[...]|...], in 0.110 sec.
mklist(22), size 88, is [[...]|...], in 0.421 sec.
mklist(24), size 96, is [[...]|...], in 43.105 sec.
mklist(26), size 104,
Crash dump was written to: erl_crash.dump
eheap_alloc: Cannot allocate 3280272216 bytes of memory (of type "heap").
rlwrap: warning: erl killed by SIGABRT.
rlwrap has not crashed, but for transparency,
it will now kill itself (without dumping core)with the same signal

很明显看到有这行代码的版本不仅执行时间长,而且需要大量内存.

为什么会出现这种情况呢?就是上面提到的"Loss of sharing",为什么会触发数据展开(或者说数据平铺化)呢?之前我们曾经聊过关于io:format的事情( [Erlang 0041] 详解io:format [链接]),在Erlang/OTP中I/O是通过向I/O Server发起I/O请求实现的.io:format调用实际上是向I/O 发送了一个io request消息,剩下的就由IO Server处理了.虽然上面的L被简略的输出为" [[...]|...]"但是在消息的传递过程中已经触发了数据平铺然后拷贝;

这种问题实际上是非常隐蔽的,所以通过宏选项在release生成的时候清理掉所有io:format输出是非常有必要的;

时间: 2024-10-22 22:16:52

[Erlang 0127] Term sharing in Erlang/OTP 上篇的相关文章

[Erlang 0128] Term sharing in Erlang/OTP 下篇

继续昨天的话题,昨天提到io:format对数据共享的间接影响,如果是下面两种情况恐怕更容易成为"坑", 呃,恰好我都遇到过; 如果是测试代码是下面这样,得到的结果会是怎样?猜! s2()-> L=[1,2,3,4,5,6], L2=[L,L,L,L], erlang:display( {{erts_debug:size(L),erts_debug:flat_size(L)},{erts_debug:size(L2),erts_debug:flat_size(L2)}} ). 结

Erlang基础 -- 介绍 -- 历史及Erlang并发

前言 最近在总结一些Erlang编程语言的基础知识,拟系统的介绍Erlang编程语言,从基础到进阶,然后再做Erlang编程语言有意思的库的分析. 其实,还是希望越来越多的人关注Erlang,使用Erlang,壮大Erlang编程语言的社区. 说实话,我也没这么高尚,就是看到很多人对Erlang编程语言的误解,Erlang编程语言社区的凋零,招个Erlang开发难之又难,才萌生此念. 这次主要介绍Erlang编程语言.包括Erlang的简要历史以及应用场景,Erlang并发编程,Erlang编程

[Erlang 0109] From Elixir to Erlang Code

Elixir代码最终编译成为erlang代码,这个过程是怎样的?本文通过一个小测试做下探索. 编译一旦完成,你就看到了真相 Elixir代码组织方式一方面和Erlang一样才用非常扁平的代码模块结构,另一方面Elixir同时支持嵌套.Elixir比较方便的一点是可以在Elixir Shell中完成对模块的定义.看下面的方式: iex> defmodule Math do ...> def sum(a, b) do ...> a + b ...> end ...> end ie

erlang字符串动态解析成为Erlang数据结构(去掉“”)

Eshell V5.8.2 (abort with ^G)1> {ok, Scan1, _} = erl_scan:string("[a,b,c].").{ok,[{'[',1},{atom,1,a},{',',1},{atom,1,b},{',',1},{atom,1,c},{']',1},{dot,1}],1}2> {ok,P}=erl_parse:parse_exprs(Scan1).{ok,[{cons,1,{atom,1,a},{cons,1,{atom,1,b}

Erlang 104 OTP - incomplete

笔记系列 Erlang环境和顺序编程Erlang并发编程Erlang分布式编程YawsErlang/OTP 日期              变更说明 2014-12-21 A Outline, 1 Agenda 0 Scope 围绕OTP设计原则,分别记录行为模式.监督树概念.应用.发布和部署,以及[3]中一个产品级缓存解决方案. 1 OTP Design Principles Erlang Doc: OTP Design Principles User's Guide OTP设计原则阐述的是如

Erlang OTP学习(1)gen_server

在<Programming Erlang>的OTP introduction章节中,作者通过循序渐进的方式,向我们展示了gen_server设计思路,现在做下总结: 在具体看gen_server之前,我们先看一个server通用框架:  在这个server里,你几乎看不到任何和具体功能相关的东西,它只提供了一个server所具备的基本框架,那它是如何运行的呢? 当我们调用start函数时,就启动了一个服务,如果服务器接收到一条消息,那么它将会把这条消息转交给Mod:handle_call或者M

1.Erlang/OTP平台

?理解并发和Erlang的进程模型 ?Erlang的容错与分布式支持 ?Erlang运行时系统的重要属性 ?什么是函数式编程,如何用Erlang进行函数式编程 1.并发:只有同时发生的任务才能算是并发任务,并发不完全是并行 在Erlang中,并发的基本单位是进程.进程拥有自己的工作内存空间和自己的信箱,信箱用于存放外来消息:而许多其他语言和操作系统中的线程却是共享相同内存空间的并发活动(随之而来的是层出不穷的互踩脚趾的机会).因此与线程相比,Erlang进程更加安全.故可以说进程封装了状态. 2

[Erlang 0126] 我们读过的Erlang论文

我在Erlang Resources 豆瓣小站上发起了一个征集活动 [链接] ,"[征集] 我们读过的Erlang论文",希望大家来参加.发起这样一个活动的目的是因为Erlang相关的出版物很少,很多时候都是从学术论文中寻找答案,而发现合适的论文是第一步,这个活动就是为了解决这个问题. 在一个极小的知识点可能都会有一篇精彩的论文为你条分缕析,抽丝剥茧,甚至可以拼凑起来一个完整的Erlang知识系统,我们开始吧... <面向软件错误构建可靠的分布式系统> Making rel

[Erlang 0129] Erlang 杂记 VI

把之前阅读资料的时候记下的东西,整理了一下. Adding special-purpose processor support to the Erlang VM   P23 简单介绍了Erlang Compiler和Beam文件格式; The Erlang Compiler in short 章节提到了 Core Erlang 这个之前有提到过: [Erlang 0120] Know a little Core Erlang http://www.cnblogs.com/me-sa/p/know