【译】构造和匹配二进制(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部分的大小设置为存储在二进制中的数据的大小,而二进制对象分配了额外的空间。二进制对象的大小是Bin1256的大小的两倍,以较大者为准。在这种情况下为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中二进制处理的信息。

posted @ 2020-06-12 14:30  云彩草原  阅读(394)  评论(0编辑  收藏  举报