首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

4.构造和匹配二进制文件 | 4. Constructing and Matching Binaries

二进制文件可以通过以下方式高效地构建:

DO

代码语言:javascript
复制
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

代码语言:javascript
复制
my_binary_to_list(<<H,T/binary>>) ->
    [H|my_binary_to_list(T)];
my_binary_to_list(<<>>) -> [].

4.1 如何实现二进制文件

在内部,二进制文件和位串以相同的方式实现。在本节中,它们被称为二进制文件,因为它们是在模拟器源代码中调用的。

内部有四种类型的二进制对象可用:

  • 两个是二进制数据的容器,被称为:
代码语言:txt
复制
-  **Refc binaries** (short for **reference-counted binaries**)
- **Heap binaries**
  • 两个仅仅是引用一个二进制文件的一部分,并被称为:
代码语言:txt
复制
- **sub binaries**
- **match contexts**

Refc二进制文件

Refc二进制文件由两部分组成:

  • 存储在进程堆中的对象称为ProcBin
  • 二进制对象本身存储在所有进程堆外部

二进制对象可以被任意数量的进程中的任意数量的ProcBins引用。该对象包含一个引用计数器,用于跟踪引用的数量,以便在最后一个引用消失时将其删除。

进程中的所有ProcBin对象都是链表的一部分,因此当ProcBin消失时,垃圾收集器可以跟踪它们并减少二进制文件中的引用计数器。

堆二进制文件

堆二进制文件是最小64字节的小型二进制文件,并直接存储在进程堆中。当进程被垃圾回收并且它们作为消息被发送时,它们被复制。他们不需要垃圾收集器的任何特殊处理。

辅助二进制文件

引用对象子二进制文件匹配上下文可以引用refc二进制或堆二进制文件的一部分。

一个二进制文件是在split_binary/2二进制文件中以二进制模式匹配时创建的。子二进制是对另一个二进制的一部分的引用(refc或heap二进制文件,但永远不会进入另一个二进制文件)。因此,匹配一个二进制文件相对便宜,因为实际的二进制数据从不被复制。

匹配上下文

匹配上下文类似于子二进制,但对于二进制匹配被优化。例如,它包含一个直接指向二进制数据的指针。对于与二进制匹配的每个字段,匹配上下文中的位置递增。

编译器试图避免生成创建子二进制文件的代码,仅在此后不久创建新的匹配上下文并丢弃子二进制文件。而不是创建一个子二进制文件,匹配上下文保持不变。

编译器只有在知道匹配上下文不会被共享的情况下才能进行这种优化。如果它将被共享,Erlang的功能属性(也称为参考透明度)将会中断。

4.2构建二进制文件

附加到二进制或位串是由运行时系统特别优化的:

代码语言:javascript
复制
<<Binary/binary, ...>>
<<Binary/bitstring, ...>>

由于运行时系统处理优化(而不是编译器),因此很少有优化不起作用的情况。

为了解释它是如何工作的,让我们逐行检查以下代码:

代码语言:javascript
复制
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 binaryBin0变量。
  • 第2行是附加操作。由于Bin0没有涉及追加操作,refc binary所以创建了一个新的内容并将其内容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写入二进制文件,因为这会改变Bin3to 的值<<0,1,2,3,4,17,6,7,8,9>>

运行时系统认为这Bin1是前一个追加操作的结果(不是来自最近的追加操作),因此它将内容复制Bin1到新的二进制文件,保留额外的存储空间等等。(这里没有解释运行时系统如何知道它是不允许写入的Bin1;它只是一个练习,让好奇的读者通过主要阅读模拟器资源来弄清楚它是如何完成的erl_bits.c。)

强制复制的情况

二进制追加操作的优化要求,有一个单一 ProcBin和一个单一的参考到ProcBin用于二进制。原因是二进制对象可以在追加操作期间移动(重新分配),并且当发生这种情况时,必须更新ProcBin中的指针。如果将有多个ProcBin指向二进制对象,则不可能找到并更新所有这些对象。

因此,对二进制文件的某些操作会将其标记为未来的任何附加操作将被迫复制二进制文件。在大多数情况下,二进制对象将同时收缩以回收分配用于增长的额外空间。

当如下追加到二进制文件时,只有从最新追加操作返回的二进制文件才会支持进一步的廉价追加操作:

代码语言:javascript
复制
Bin = <<Bin0,...>>

在本节开头的代码片段中,追加到Bin将是便宜的,而追加Bin0将强制创建一个新的二进制文件并复制其内容Bin0

如果二进制文件作为消息发送到进程或端口,则二进制文件将被收缩,并且任何进一步的附加操作都会将二进制数据复制到新的二进制文件中。例如,在下面的代码片段Bin1将被复制到第三行:

代码语言:javascript
复制
Bin1 = <<Bin0,...>>,
PortOrPid ! Bin1,
Bin = <<Bin1,...>>  %% Bin1 will be COPIED

如果您将一个二进制文件插入Ets表中,将其发送到使用端口erlang:port_command/2或将其传递到enif_inspect_binaryNIF 中,则会发生同样的情况。

匹配二进制文件也会导致它缩小,下一个附加操作将复制二进制数据:

代码语言:javascript
复制
Bin1 = <<Bin0,...>>,
<<X,Y,Z,T/binary>> = Bin1,
Bin = <<Bin1,...>>  %% Bin1 will be COPIED

原因是match context包含一个直接指向二进制数据的指针。

如果一个进程只是保留二进制文件(无论是在“循环数据”还是在进程字典中),那么垃圾收集器最终可以缩小二进制文件。如果只保留一个这样的二进制文件,它将不会收缩。如果该进程稍后附加到已缩小的二进制文件,则将重新分配二进制对象以使数据被附加。

4.3 匹配二进制文件

让我们回顾一下上一节开头的例子:

DO

代码语言:javascript
复制
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遍历整个二进制文件时,匹配上下文已被丢弃。如果迭代在到达二进制结束之前停止,会发生什么?优化是否仍然有效?

代码语言:javascript
复制
after_zero(<<0,T/binary>>) ->
    T;
after_zero(<<_,T/binary>>) ->
    after_zero(T);
after_zero(<<>>) ->
    <<>>.

是的,它会。编译器将删除第二个子句中的子二进制文件的构建:

代码语言:javascript
复制
...
after_zero(<<_,T/binary>>) ->
    after_zero(T);
...

但它会生成在第一个子句中构建子二进制文件的代码:

代码语言:javascript
复制
after_zero(<<0,T/binary>>) ->
    T;
...

因此,after_zero/1构建一个匹配上下文和一个子二进制文件(假设它传递了一个包含零字节的二进制文件)。

下面的代码也将被优化:

代码语言:javascript
复制
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已经是二进制文件,则不执行任何操作)。

在开始认为编译器可以优化任何二进制模式之前,编译器无法优化以下函数(当前至少):

代码语言:javascript
复制
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

代码语言:javascript
复制
erlc +bin_opt_info Mod.erl

或通过一个环境变量传递:

代码语言:javascript
复制
export ERL_COMPILER_OPTIONS=bin_opt_info

请注意,这bin_opt_info并不意味着它会成为永久性选项Makefile,因为它所生成的所有消息都无法消除。因此,在大多数情况下,通过环境传递选项是最实际的方法。

警告如下所示:

代码语言:javascript
复制
./efficiency_guide.erl:60: Warning: NOT OPTIMIZED: sub binary is used or returned
./efficiency_guide.erl:62: Warning: OPTIMIZED: creation of sub binary delayed

为了更清楚地说明警告引用的代码,以下示例中的警告将作为注释插入到它们引用的子句后面,例如:

代码语言:javascript
复制
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(<<>>) ->
    <<>>.

第一个条款的警告说,创建一个子二进制文件不能被延迟,因为它会被返回。第二个子句的警告说,一个子二进制文件将还不会被创建。

让我们重新回顾一下无法优化的代码的先前示例,并找出原因:

代码语言:javascript
复制
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都不能进行延迟的子二进制优化。还有一个改变参数顺序的建议。第二个警告(恰好涉及同一行)是指构建子二进制本身。

不久之后,另一个例子将显示警告INFONOT OPTIMIZED警告之间的区别更清楚一点,但让我们首先按照建议更改参数顺序:

代码语言:javascript
复制
opt_eq(<<H,T1/binary>>, [H|T2]) ->
        %% OPTIMIZED: creation of sub binary delayed
    opt_eq(T1, T2);
opt_eq(<<_,_/binary>>, [_|_]) ->
    false;
opt_eq(<<>>, []) ->
    true.

编译器给出了以下代码片段的警告:

代码语言:javascript
复制
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/2match_body/2或者来自另一个函数中的另一个子句)的调用,则延迟的子二进制优化将不可能。更多的警告将发生在任何一个地方,其中一个子二进制在结尾匹配并作为第二个参数传递给match_body/2,例如:

代码语言:javascript
复制
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).

未使用的变量

编译器会判断变量是否未被使用。为以下每个函数生成相同的代码:

代码语言:javascript
复制
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中二进制处理的一些信息。

扫码关注腾讯云开发者

领取腾讯云代金券

http://www.vxiaotou.com