【译】构造和匹配二进制(Efficiency Guide)
可以通过以下方式有效地构建二进制:
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.
二进制可以像这样有效地匹配:
my_binary_to_list(<<H,T/binary>>) ->
[H|my_binary_to_list(T)];
my_binary_to_list(<<>>) -> [].
4.1如何实现二进制
在内部,二进制和位串以相同的方式实现。在本节中,它们被称为二进制,因为这就是它们在模拟器源代码中的名称。
内部有四种类型的二进制对象:
-
两个是二进制数据的容器,它们被称为:
- Refc Binaries(引用计数二进制的缩写)
- 堆二进制
-
两个仅仅是对二进制一部分的引用,它们被称为:
- 子二进制
- 匹配上下文
Refc二进制
Refc二进制包含两个部分:
- 存储在进程堆上的对象,称为ProcBin
- 二进制对象本身,存储在所有进程堆的外部
二进制对象可由任意数量的进程中的任意数量的ProcBins引用。该对象包含一个引用计数器,以跟踪引用的数量,以便在最后一个引用消失时可以将其删除。
进程中的所有ProcBin对象都是链接列表的一部分,因此,当ProcBin消失时,垃圾收集器可以跟踪它们并减少二进制中的引用计数器。
堆二进制
堆二进制是小型二进制,最多64个字节,并直接存储在进程堆中。当进程被垃圾回收并且作为消息发送时,它们被复制。它们不需要垃圾收集器进行任何特殊处理。
子二进制
引用对象子二进制和匹配上下文可以引用refc二进制或堆二进制的一部分。
子二进制通过创建split_binary/2,并且当二进制以二进制模式匹配的。子二进制是对另一个二进制(refc或堆二进制,而不是对另一个子二进制)的一部分的引用。因此,匹配二进制相对便宜,因为从不复制实际的二进制数据。
匹配上下文
匹配上下文类似于子二进制,但对于二进制匹配被优化。例如,它包含一个指向二进制数据的直接指针。对于从二进制中匹配的每个字段,匹配上下文中的位置会增加。
编译器试图避免生成用于创建子二进制的代码,而只是在不久之后创建一个新的匹配上下文并丢弃该子二进制。保留匹配上下文,而不是创建子二进制。
如果编译器知道不会共享匹配上下文,则只能进行此优化。如果将其共享,则Erlang的功能属性(也称为参照透明性)将中断。
4.2构造二进制
运行时系统特别优化了附加到二进制或位串的操作:
<<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行(标有%% 1注释)将堆二进制分配给Bin0变量。
- 第2行是追加操作。由于Bin0尚未参与追加操作,新的REFC二进制被创建和内容Bin0被复制到它。refc二进制的ProcBin部分的大小设置为存储在二进制中的数据的大小,而二进制对象分配了额外的空间。二进制对象的大小是Bin1或256的大小的两倍,以较大者为准。在这种情况下为256。
- 第3行更有趣。Bin1已经在附加操作中使用,并且它具有252个字节在末端未使用的存储空间,所以3个新字节被存储在那里。
- 第4行。此处同样适用。剩下249个字节,因此存储另外3个字节没有问题。
- 第5行。在这里,发生了一些有趣的事情。请注意,结果不追加到以前的结果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>>。
运行时系统会发现Bin1是上一个追加操作(不是最新的追加操作)的结果,因此它将Bin1的内容复制到新的二进制中,保留了额外的存储空间,依此类推。(这里没有解释运行时系统如何知道不允许将其写入Bin1;好奇的读者可以将其作为练习,通过读取仿真器源代码(主要是erl_bits.c)来了解如何完成此操作。)
强制复制的情况
二进制追加操作的优化要求,有一个单一ProcBin和一个单一的引用到ProcBin用于二进制。原因是可以在追加操作期间移动(重新分配)二进制对象,并且在这种情况下,必须更新ProcBin中的指针。如果将有多个ProcBin指向二进制对象,则将不可能找到并更新所有它们。
因此,对二进制的某些操作会对其进行标记,以便将来任何附加操作都将被强制复制二进制。在大多数情况下,二进制对象将同时缩小以回收分配给增长的额外空间。
当按如下所示追加到二进制时,仅从最新的追加操作返回的二进制将支持进一步的廉价追加操作:
Bin = <<Bin0,…>>
在本节开头的代码片段中,追加到Bin将很便宜,而追加到Bin0将强制创建新的二进制并复制Bin0的内容。
如果将二进制作为消息发送到进程或端口,则该二进制将缩小,并且任何进一步的追加操作会将二进制数据复制到新的二进制中。例如,在下面的代码片段中,Bin1将被复制到第三行:
Bin1 = <<Bin0,…>>,
PortOrPid!Bin1
Bin = <<Bin1,…>> %% Bin1将被复制
如果将二进制插入到Ets表中,或者使用erlang:port_command/2将其发送到端口,或者将其传递给NIF中的enif_inspect_binary,也会发生同样的情况。
匹配二进制也将导致其缩小,并且下一个追加操作将复制二进制数据:
Bin1 = <<Bin0,…>>,
<< X,Y,Z,T/binary>> = Bin1,
Bin = <<Bin1,…>> %% Bin1将被复制
原因是匹配上下文包含指向二进制数据的直接指针。
如果进程仅保留二进制(在“循环数据”中或在进程字典中),则垃圾收集器最终可以收缩二进制。如果只保留一个这样的二进制,它将不会缩小。如果该过程稍后追加到已缩小的二进制中,则将重新分配二进制对象以放置要附加的数据。
4.3匹配二进制
让我们回顾上一节开头的示例:
my_binary_to_list (<< H,T/binary >>) ->
[H | my_binary_to_list(T)];
my_binary_to_list (<< >>) -> []。
首次调用my_binary_to_list/1时,将创建一个匹配上下文。匹配上下文指向二进制的第一个字节。1个字节被匹配,并且匹配上下文被更新以指向二进制中的第二个字节。
在这一点上,创建一个子二进制是有意义的,但是在此特定示例中,编译器发现很快将调用一个函数(在本例中为my_binary_to_list/1本身),该函数将立即创建一个新的匹配上下文并丢弃子二进制。
因此,my_binary_to_list/1会使用match上下文而不是子二进制进行调用。初始化匹配操作的指令在看到已传递给匹配上下文而不是二进制时,基本上什么也不做。
当到达二进制的末尾并且第二个子句匹配时,匹配上下文将被简单地丢弃(在下一个垃圾回收中将其删除,因为不再有对其的引用)。
总而言之,my_binary_to_list/1仅需要创建一个匹配上下文,而无需子二进制。
请注意,遍历整个二进制后,将放弃my_binary_to_list/1中的match上下文。如果迭代在到达二进制末尾之前停止,会发生什么情况?优化是否仍然有效?
after_zero(<<0,T/binary>>) ->
T;
after_zero(<<_,T/binary>>) ->
after_zero(T);
after_zero(<<>>) ->
<<>>.
是的,它会的。编译器将在第二个子句中删除子二进制的构建:
…
after_zero(<<_,T/binary>>) ->
after_zero(T);
…
但是它将生成在第一个子句中构建子二进制的代码:
after_zero(<<0,T/binary>>) ->
T;
…
因此,after_zero/1构建一个匹配上下文和一个子二进制(假定传递了一个包含零字节的二进制)。
如下代码也将得到优化:
all_but_zeroes_to_list(Buffer, Acc, 0) ->
{lists:reverse(Acc),Buffer};
all_but_zeroes_to_list(<<0,T/binary>>, Acc, Remaining) ->
all_but_zeroes_to_list(T, Acc, Remaining-1);
all_but_zeroes_to_list(<<Byte,T/binary>>, Acc, Remaining) ->
all_but_zeroes_to_list(T, [Byte|Acc], Remaining-1).
编译器在第二和第三子句中删除了子二进制的构建,并向第一子句添加了一条指令,该指令将Buffer从匹配上下文转换为子二进制(如果Buffer已经是二进制,则不执行任何操作)。
但是在更复杂的代码中,如何知道是否应用了优化呢?
选项bin_opt_info
使用bin_opt_info选项可使编译器打印许多有关二进制优化的信息。可以将其提供给编译器或erlc:
erlc +bin_opt_info Mod.erl
或通过环境变量传递:
export ERL_COMPILER_OPTIONS=bin_opt_info
注意,bin_opt_info并不是要添加到Makefile的永久选项,因为它生成的所有消息都无法消除。因此,在大多数情况下,将选项传递给环境是最实用的方法。
警告如下:
./efficiency_guide.erl:60:警告:未优化:该函数返回了二进制
./efficiency_guide.erl:62:警告:已优化:匹配上下文已重用
为了更清楚地说明警告所指的代码,例如,以下示例中的警告以注释的形式插入它们所引用的子句之后,例如:
after_zero (<< 0,T/binary>>) -> %% BINARY CREATED:从函数返回二进制
T;
after_zero (<< _,T/binary >>) -> %%优化:重用匹配上下文
after_zero(T);
after_zero (<< >>) ->
<< >>。
第一个子句的警告说,不能延迟子二进制的创建,因为它将被返回。第二个子句的警告说将不会创建子二进制。
未使用的变量
编译器会确定变量是否未使用。为以下每个功能生成相同的代码:
count1(<<_,T/binary>>, Count) -> count1(T, Count+1);
count1(<<>>, Count) -> Count.
count2(<<H,T/binary>>, Count) -> count2(T, Count+1);
count2(<<>>, Count) -> Count.
count3(<<_H,T/binary>>, Count) -> count3(T, Count+1);
count3(<<>>, Count) -> Count.
在每次迭代中,二进制中的前8位将被跳过,不匹配。
4.4历史记录
R12B中的二进制处理得到了显着改善。由于在R11B中有效的代码在R12B中可能无效,反之亦然,因此本《效率指南》的较早版本包含一些有关R11B中二进制处理的信息。