[Erlang-0004][OTP] 高效指南 -- 二进制的构造和匹配(1)
原文链接:http://www.erlang.org/doc/efficiency_guide/binaryhandling.html
(水平有限,错误之处欢迎指正)
4 二进制的构造和匹配
在R12B中,构造和匹配二进制比之前的版本要快很多。
你可以轻松的写出一个二进制结构。
DO (in R12B) / REALLY DO NOT (in earlier releases)
my_list_to_binary(List) -> my_list_to_binary(List, <<>>). my_list_to_binary([H|T], Acc) -> my_list_to_binary(T, <<Acc/binary,H>>); my_list_to_binary([], Acc) -> Acc.
在R12B版本之前,每次迭代都会拷贝Acc。而在R12B中,Acc只有在第一次迭代时才会被拷贝,同时在拷贝末尾会预先分配一块额外内存空间。在接下来的迭代中,H会被写在预先分配的空间中。空间满了就再分配一块。
额外内存空间的大小取决于当前二进制数据大小的两倍和256(字节)哪个更大,按照较大的来分配。
最常用的二进制匹配方式也是现在最快的:
DO (in R12B)
my_binary_to_list(<<H,T/binary>>) -> [H|my_binary_to_list(T)]; my_binary_to_list(<<>>) -> [].
4.1 二进制是怎样被实现的
二进制和位字符串底层实现是相同的。本节我们统称他们为“二进制”,其实在虚拟机的源码中就是这么称呼它们的。
底层有四种类型的二进制对象,其中有两种是真正的二进制数据的容器,而另外两种只不过是对二进制的引用。
二进制容器被称作refc binaries(refernce-counted binaries的简称)和heap binaries。
Refc binaries分为两部分,引用部分存放在进程的堆上,叫做ProcBin,而二进制对象存放在所有进程堆之外。
二进制对象能够被任意数量的任意ProBins引用,它维护者一个引用计数,当引用计数为0时,能被回收。
每个进程里的所有ProBin都是一个链表的一部分,以便垃圾回收器能够持续跟踪它们,在ProBin消失时相应减小二进制对象的引用计数。
Heap binaries非常小,最多64字节,直接被保存在进程的堆上。当所在的进程被垃圾回收,或作为消息被发送的时候,heap binaries会被拷贝。heap binaries只能遵循常规的垃圾回收机制,没有特殊照顾。
有两种引用对象可以引用refc binary和heap binary,被称作subbinary和match contexts。
Sub binary由split_binary/2或二进制模式匹配产生,它引用一个二进制(refc或者heap binary,永远不会是另一个sub binary)对象的一部分。所以匹配出一个二进制代价相当低,因为二进制对象没有被拷贝。
match context和sub binary类似,但它为二进制匹配做了优化,例如,它包含一个直接的指针,指向从二进制数据中匹配出的每一个“字段”,因此match context的大小会增加。(原文:A match context is similar to a sub binary, but is optimized for binary matching; for instance, it contains a direct pointer to the binary data. For each field that is matched out of a binary, the position in the match context will be incremented.)
在R11B中,match context只会用在二进制的匹配操作上。
在R12B中,编译器尽量不生成创建sub binary的代码。后续会更多的使用match context,少用sub binary,最终match context将会替代sub binary。
The compiler can only do this optimization if it can know for sure that the match context will not be shared. If it would be shared, the functional properties (also called referential transparency) of Erlang would break.(求翻译)
4.2 构造二进制
在R12B中,运行时系统中被特别优化了二进制和位字符串的追加操作,因为是在运行时优化,而不是在编译时,因此这些优化几乎不会不生效。
<<Binary/binary, ...>> <<Binary/bitstring, ...>>
为了解释它是怎么工作的,我们来逐行看下面的代码
Bin0 = <<0>>, %% 1 Bin1 = <<Bin0/binary,1,2,3>>, %% 2 Bin2 = <<Bin1/binary,4,5,6>>, %% 3 Bin3 = <<Bin2/binary,7,8,9>>, %% 4 Bin4 = <<Bin1/binary,17>>, %% 5 !!! {Bin4,Bin3} %% 6
第一行(注释%%1),为变量Bin0分配了一个heap binary.
第二行是一个追加操作,由于Bin0之前没有进行过追加操作,所以一个新的refc binary被创建,并且会将Bin0的数据复制过去。Refc binary的ProcBin的大小相当于存储在二进制中的数据的大小,而二进制对象将会再分配一块额外的空间,额外空间的大小等于两倍数据和256之间较大一个,这里取256.
第三行更有趣,Bin1已经有过追加操作,而且在它末尾有255个字节可以用来存储(还有1字节是拷贝过来的0),所以新增的三个字节被储存在这里面。
第四行相同,有252字节剩余,所以再存3个也没关系。
但是在第五行,有趣的事情发生了,我们没有追加到前一行的结果Bin3,而是Bin1。我们希望Bin4变成<<0,1,2,3,17>>,Bin3保持原样<<0,1,2,3,4,5,6,7,8,9>>。但是很明显,运行时系统不会把“17”写道二进制里,因为那会把Bin3变成<<0,1,2,3,4,17,6,7,8,9>>。(靖阳:这里不理解为啥是这样的,而不是<<0,1,2,3,17,5,6,7,8,9>>)
那么会发生什么呢?
运行时系统会看到Bin1不是上一步追加操作的结果,因此会拷贝Bin1然后申请新的额外存储空间。(这里先不解释运行时系统是如何知道Bin1是不可写的;作为一个练习留给有心的读者,大家可以通过阅读虚拟机源码来找到这是如何实现的,主要是erl_bits.c。)
强制拷贝的情况
二进制追加操作的优化要求二进制数据只能有一个ProcBin的引用。因为二进制对象在进行追加操作的时候会重新分配,当这种情况发生时,ProcBin的指针必须更新。如果有一个以上的ProcBin指向同一个二进制对象,想全部更新是不可能的。
因此,二进制的某些操作会对其进行标记,以便之后的追加操作都必须强制拷贝。大多数情况,二进制对象会被压缩同时会回收那些预先分配了的空间。
Bin = <<Bin0,...>>
当对二进制进行追加操作时,只有上一次追加操作返回的二进制才能继续支持低代价的追加操作。上面的代码片段,对Bin继续进行追加操作的代价很低,但对Bin0进行追加时,会强制新建一个二进制对象,并将Bin0的内容拷贝过去。
如果二进制对象被发送给一个进程或端口,它会被压缩,而且后续任何追加操作都会拷贝二进制数据到一个新的二进制对象中。例如,下面的代码
Bin1 = <<Bin0,...>>, PortOrPid ! Bin1, Bin = <<Bin1,...>> %% Bin1 will be COPIED
Bin1在第三行会被拷贝。
相同的情况会发生在将二进制插入ets时或用erlang:port_command/2命令发送到端口时。
匹配二进制一会使其被压缩,而且下一次追加操作会拷贝二进制数据:
Bin1 = <<Bin0,...>>, <<X,Y,Z,T/binary>> = Bin1, Bin = <<Bin1,...>> %% Bin1 will be COPIED
原因是match context包含一个直接指向二进制数据的指针。
如果进程只是简单存储二进制数(无论是"loop data"还是在进程字典中),垃圾回收器最终会压缩二进制。如果只有一个这样的二进制被保存,他不会被压缩。如果进程接下来对已经被压缩的二进制进行追加操作,二进制对象会被重新分配,为追加的数据提供空间。
(原创翻译,欢迎任何形式的转载,但请务必注明出处:http://www.cnblogs.com/liangjingyang)