4.构造和匹配二进制文件 | 4. Constructing and Matching Binaries
4 构造和匹配二进制文件
二进制文件可以通过以下方式高效地构建:
DO
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.
二进制文件可以像这样高效匹配:
DO
my_binary_to_list(<<H,T/binary>>) ->
[H|my_binary_to_list(T)];
my_binary_to_list(<<>>) -> [].
4.1 如何实现二进制文件
在内部,二进制文件和位串以相同的方式实现。在本节中,它们被称为二进制文件,
因为它们是在模拟器源代码中调用的。
内部有四种类型的二进制对象可用:
- 两个是二进制数据的容器,被称为:
- **Refc binaries** (short for **reference-counted binaries**)
- **Heap binaries**
- 两个仅仅是引用一个二进制文件的一部分,并被称为:
- **sub binaries**
- **match contexts**
Refc二进制文件
Refc二进制文件由两部分组成:
- 存储在进程堆中的对象称为
ProcBin
二进制对象可以被任意数量的进程中的任意数量的ProcBins引用。该对象包含一个引用计数器,用于跟踪引用的数量,以便在最后一个引用消失时将其删除。
进程中的所有ProcBin对象都是链表的一部分,因此当ProcBin消失时,垃圾收集器可以跟踪它们并减少二进制文件中的引用计数器。
堆二进制文件
堆二进制文件是最小64字节的小型二进制文件,并直接存储在进程堆中。当进程被垃圾回收并且它们作为消息被发送时,它们被复制。他们不需要垃圾收集器的任何特殊处理。
辅助二进制文件
引用对象子二进制文件
和匹配上下文
可以引用refc二进制或堆二进制文件的一部分。
一个二进制文件
是在split_binary/2
二进制文件
中以二进制模式匹配时创建的。子二进制是对另一个二进制的一部分的引用(refc或heap二进制文件
,但永远不会进入另一个二进制文件
)。因此,匹配一个二进制文件
相对便宜,因为实际的二进制数据从不被复制。
匹配上下文
甲匹配上下文
类似于子二进制,但对于二进制匹配被优化。例如,它包含一个直接指向二进制数据的指针。对于与二进制匹配的每个字段,匹配上下文
中的位置递增。
编译器试图避免生成创建子二进制文件的代码,仅在此后不久创建新的匹配上下文并丢弃子二进制文件。而不是创建一个子二进制文件,匹配上下文保持不变。
编译器只有在知道匹配上下文不会被共享的情况下才能进行这种优化。如果它将被共享,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
注释),分配heap binary
给Bin0
变量。
运行时系统认为这Bin1
是前一个追加操作的结果(不是来自最近的追加操作),因此它将内容复制
Bin1
到新的二进制文件,保留额外的存储空间等等。(这里没有解释运行时系统如何知道它是不允许写入的Bin1
;它只是一个练习,让好奇的读者通过主要阅读模拟器资源来弄清楚它是如何完成的erl_bits.c
。)
强制复制的情况
二进制追加操作的优化要求,有一个单一
ProcBin和一个单一的参考
到ProcBin用于二进制。原因是二进制对象可以在追加操作期间移动(重新分配),并且当发生这种情况时,必须更新ProcBin中的指针。如果将有多个ProcBin指向二进制对象,则不可能找到并更新所有这些对象。
因此,对二进制文件的某些操作会将其标记为未来的任何附加操作将被迫复制二进制文件。在大多数情况下,二进制对象将同时收缩以回收分配用于增长的额外空间。
当如下追加到二进制文件时,只有从最新追加操作返回的二进制文件才会支持进一步的廉价追加操作:
Bin = <<Bin0,...>>
在本节开头的代码片段中,追加到Bin
将是便宜的,而追加Bin0
将强制创建一个新的二进制文件并复制其内容Bin0
。
如果二进制文件作为消息发送到进程或端口,则二进制文件将被收缩,并且任何进一步的附加操作都会将二进制数据复制到新的二进制文件中。例如,在下面的代码片段Bin1
将被复制到第三行:
Bin1 = <<Bin0,...>>,
PortOrPid ! Bin1,
Bin = <<Bin1,...>> %% Bin1 will be COPIED
如果您将一个二进制文件插入Ets表中,将其发送到使用端口erlang:port_command/2
或将其传递到enif_inspect_binary
NIF 中,则会发生同样的情况。
匹配二进制文件也会导致它缩小,下一个附加操作将复制二进制数据:
Bin1 = <<Bin0,...>>,
<<X,Y,Z,T/binary>> = Bin1,
Bin = <<Bin1,...>> %% Bin1 will be COPIED
原因是match context
包含一个直接指向二进制数据的指针。
如果一个进程只是保留二进制文件(无论是在“循环数据”还是在进程字典中),那么垃圾收集器最终可以缩小二进制文件。如果只保留一个这样的二进制文件,它将不会收缩。如果该进程稍后附加到已缩小的二进制文件,则将重新分配二进制对象以使数据被附加。
4.3 匹配二进制文件
让我们回顾一下上一节开头的例子:
DO
my_binary_to_list(<<H,T/binary>>) ->
[H|my_binary_to_list(T)];
my_binary_to_list(<<>>) -> [].
第一次my_binary_to_list/1
被称为,一个match context
被创建。匹配上下文指向二进制文件的第一个字节。1个字节匹配出来,并且匹配上下文更新为指向二进制中的第二个字节。
在这一点上,创建一个是有意义的sub binary
,但在这个特定的例子中,编译器会发现不久将会调用一个函数(在本例中为my_binary_to_list/1
自己),它立即创建一个新的匹配上下文并丢弃该子二进制文件。
因此my_binary_to_list/1
使用匹配上下文而不是子二进制来调用它自己。初始化匹配操作的指令在看到它传递了匹配上下文而不是二进制时基本上什么都不做。
当达到二进制的结尾并且第二个子句匹配时,匹配上下文将被简单地丢弃(在下一个垃圾回收中被删除,因为不再有任何引用)。
总而言之,my_binary_to_list/1
只需要创建一个
匹配上下文并且不需要子二进制文件。
请注意,my_binary_to_list/1
遍历整个二进制文件时,匹配上下文已被丢弃。如果迭代在到达二进制结束之前停止,会发生什么?优化是否仍然有效?
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
已经是二进制文件,则不执行任何操作)。
在开始认为编译器可以优化任何二进制模式之前,编译器无法优化以下函数(当前至少):
non_opt_eq([H|T1], <<H,T2/binary>>) ->
non_opt_eq(T1, T2
non_opt_eq([_|_], <<_,_/binary>>) ->
false;
non_opt_eq([], <<>>) ->
true.
前面提到,如果编译器知道二进制文件不会被共享,那么编译器只能延迟创建子二进制文件。在这种情况下,编译器无法知道。
很快就会显示如何重写,non_opt_eq/2
以便可以应用延迟的子二进制优化,更重要的是,它显示了如何确定您的代码是否可以优化。
Option 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: Warning: NOT OPTIMIZED: sub binary is used or returned
./efficiency_guide.erl:62: Warning: OPTIMIZED: creation of sub binary delayed
为了更清楚地说明警告引用的代码,以下示例中的警告将作为注释插入到它们引用的子句后面,例如:
after_zero(<<0,T/binary>>) ->
%% NOT OPTIMIZED: sub binary is used or returned
T;
after_zero(<<_,T/binary>>) ->
%% OPTIMIZED: creation of sub binary delayed
after_zero(T
after_zero(<<>>) ->
<<>>.
第一个条款的警告说,创建一个子二进制文件不能被延迟,因为它会被返回。第二个子句的警告说,一个子二进制文件将还不会被创建。
让我们重新回顾一下无法优化的代码的先前示例,并找出原因:
non_opt_eq([H|T1], <<H,T2/binary>>) ->
%% INFO: matching anything else but a plain variable to
%% the left of binary pattern will prevent delayed
%% sub binary optimization;
%% SUGGEST changing argument order
%% NOT OPTIMIZED: called function non_opt_eq/2 does not
%% begin with a suitable binary matching instruction
non_opt_eq(T1, T2
non_opt_eq([_|_], <<_,_/binary>>) ->
false;
non_opt_eq([], <<>>) ->
true.
编译器发出两个警告。该INFO
警告指的是non_opt_eq/2
作为被调用者的函数,表示调用的任何函数non_opt_eq/2
都不能进行延迟的子二进制优化。还有一个改变参数顺序的建议。第二个警告(恰好涉及同一行)是指构建子二进制本身。
不久之后,另一个例子将显示警告INFO
和NOT OPTIMIZED
警告之间的区别更清楚一点,但让我们首先按照建议更改参数顺序:
opt_eq(<<H,T1/binary>>, [H|T2]) ->
%% OPTIMIZED: creation of sub binary delayed
opt_eq(T1, T2
opt_eq(<<_,_/binary>>, [_|_]) ->
false;
opt_eq(<<>>, []) ->
true.
编译器给出了以下代码片段的警告:
match_body([0|_], <<H,_/binary>>) ->
%% INFO: matching anything else but a plain variable to
%% the left of binary pattern will prevent delayed
%% sub binary optimization;
%% SUGGEST changing argument order
done;
...
警告意味着如果
有match_body/2
(match_body/2
或者来自另一个函数中的另一个子句)的调用,则延迟的子二进制优化将不可能。更多的警告将发生在任何一个地方,其中一个子二进制在结尾匹配并作为第二个参数传递给match_body/2
,例如:
match_head(List, <<_:10,Data/binary>>) ->
%% NOT OPTIMIZED: called function match_body/2 does not
%% begin with a suitable binary matching instruction
match_body(List, Data).
未使用的变量
编译器会判断变量是否未被使用。为以下每个函数生成相同的代码:
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中二进制处理的一些信息。