SSA 历史

2018 年 9 月 28 日 · 作者:Björn Gustavsson

这篇博客文章回顾了今年年初到 8 月底,当分支合并时,基于 SSA 的中间表示的开发过程。

2018 年 1 月 #

今年一月,我们意识到在 BEAM 代码上进行优化已经达到了极限。

John 完成了扩展 beam_bsm 的工作(一个尝试延迟创建子二进制的 pass)。扩展后的 beam_bsm pass 可以比以前在更多情况下应用优化,但是 beam_bsm 中为实现对优化的小幅改进而添加的代码量却非常庞大。

John,Lukas 和我讨论了我们应该如何处理这个问题。很明显,我们需要一个更好的中间格式。但是它应该是什么样的呢?我们是否可以使用现有的 BEAM 代码,但使用变量而不是 BEAM 寄存器,并在稍后进行寄存器分配?这可以解决一些问题,但不能解决所有问题。BEAM 指令的不规则性使得遍历和分析 BEAM 代码很麻烦。

因此,我们决定像大多数现代编译器一样,使用基于 SSA 的中间格式。

重写是可怕的! #

引入新的中间格式将需要重写编译器的至少某些部分。重写的问题在于,它们总是比预期花费更长的时间,并且经常在完成之前就被放弃。

为了增加这次重写成功的几率,我们制定了以下计划,以尽可能少的精力尽快使某些功能正常工作

  1. 编写一个新的 pass,将 Kernel Erlang 转换为 SSA 代码。

  2. 编写一个新的 pass,将 SSA 代码转换为 BEAM 代码。

  3. 保留所有现有的优化 pass。

  4. 一次重写一个优化 pass。

正如即将显而易见的,这并没有完全按照计划进行。

2018 年 2 月 #

我在今年 2 月 1 日提交了第一个 commit。

beam_kernel_to_ssa #

我编写的第一个 pass 是从 Kernel Erlang 到 SSA 代码的转换器。我们将其命名为 beam_kernel_to_ssa

我的第一个想法是从头开始编写 pass,而不是基于 v3_codegen。毕竟,BEAM 代码和 SSA 代码之间存在根本差异。BEAM 代码是指令的平面列表。SSA 代码由存储在映射中的编号块组成,并且还有 phi 节点。

另一方面,v3_codegenbeam_kernel_to_ssa 的输入都是 Kernel Erlang。处理 Kernel Erlang 记录的代码没有问题,我不想从头开始重写该代码。相反,我重写了生成 BEAM 指令列表的代码部分,以生成 SSA 指令列表。然后,我编写了一个简单的 pass(大约 100 行代码),将 SSA 指令打包到块中并添加了 phi 节点

测试 beam_kernel_to_ssa #

我喜欢在编写代码后尽快对其进行测试。在最近编写的代码中查找和修复错误要容易得多。

在编写 BEAM 代码的代码生成器之前,如何测试 beam_kernel_to_ssa

不能完全测试,但有一些方法可以找到主要问题。

一种方法是 冒烟测试。我修改了编译器,使其首先运行 beam_kernel_to_ssa 但丢弃其输出,然后运行 v3_codegen 和其余的编译器 pass。这使我可以运行整个编译器测试套件,如果 beam_kernel_to_ssa pass 崩溃了,我就找到了一个错误。

另一种方法是编写 SSA 代码的验证器或 lint 工具。John 编写了 beam_ssa_lint pass(当时实际上称为 beam_ssa_validator,后来重命名),该 pass 会验证一个变量是否只定义一次,变量是否在使用之前定义,终止符和 phi 节点中的标签是否引用已定义的块等等。它帮助我找到了一些错误。

Dialyzer 也帮助我找到了一些错误。我确保为所有新记录中的所有字段添加类型,并为所有导出的函数添加规范。当我运行 Dialyzer 时,它指出了一些错误,并且在编写 -type 声明时考虑类型也很有用。

完成 beam_kernel_to_ssa #

我不确定在 beam_kernel_to_ssa 的初始实现上花费了多少时间,但可能不到两周。在此过程中遇到了一些障碍,其中大多数是 v3_kernel 中的错误,这些错误没有给旧的 v3_codegen 带来任何问题。

这是一个示例。我选择在 OTP 21 中修复它,即使它在该版本中是无害的

v3_kernel:停止在 #k_try{} 中确保一个返回值

beam_ssa_pre_codegen #

接下来是从 SSA 代码到 BEAM 代码的转换。

我已经决定,转换过程足够复杂,最好将其分为两个主要的 pass。

这些 pass 的第一个,beam_ssa_pre_codegen,将在 SSA 代码上工作,重写它,并为另一个生成 BEAM 代码的 pass 添加注释,但是输出仍然是有效的 SSA 代码,以便可以使用 ssa_lint 来验证输出。美观打印的 SSA 代码还包括注释,以方便调试。

可以使用 dprecg 选项来生成 SSA 代码的美观打印列表。以下命令将创建文件 blog.precodegen

erlc +dprecg blog.erl

下一节将深入探讨 beam_ssa_pre_codegen 的工作原理。在第一次阅读时,您可能想跳过该部分,然后跳到有关 beam_ssa_codegen 的部分。

深入了解 beam_ssa_pre_codegen #

为了给 beam_ssa_pre_codegen 的描述提供一些上下文,我们将首先看一些 BEAM 代码,并讨论堆栈帧和 Y 寄存器。

这是 Erlang 中的示例

foo(C, L) ->
    Sum = lists:sum(L),
    C + Sum.

BEAM 代码如下所示

{allocate,1,2}.
{move,{x,0},{y,0}}.
{move,{x,1},{x,0}}.
{line,[{location,"blog.erl",5}]}.
{call_ext,1,{extfunc,lists,sum,1}}.
{line,[{location,"blog.erl",6}]}.
{gc_bif,'+',{f,0},1,[{y,0},{x,0}],{x,0}}.
{deallocate,1}.
return.

像往常一样,我们将一次或几次浏览代码。

{allocate,1,2}.

allocate 指令分配一个堆栈帧。操作数 1 表示堆栈帧中应有一个插槽用于存储一个值。堆栈帧中的插槽称为Y 寄存器

操作数 2 表示两个 X 寄存器({x,0}{x,1})处于活动状态,如果 allocate 需要进行垃圾回收以分配堆栈帧空间,则必须保留它们。

{move,{x,0},{y,0}}.

foo/2C 参数在 {x,0} 中。move 指令将 {x,0} 的值复制到堆栈帧中的第零个插槽 {y,0}。进行此复制的原因很快就会变得清楚。

{move,{x,1},{x,0}}.

为了准备调用 lists:sum/1{x,1} 中的 L 值被复制到 {x,0}

{line,[{location,"blog.erl",5}]}.
{call_ext,1,{extfunc,lists,sum,1}}.

在这里调用 lists:sum/1。参数在 {x,0} 中。结果(列表中所有数字的总和)在 {x,0} 中返回。此外,所有其他 X 寄存器的内容都将被破坏。这意味着在函数调用之后要使用的任何值都必须保存到 Y 寄存器。

{gc_bif,'+',{f,0},1,[{y,0},{x,0}],{x,0}}.

此指令计算 C(在 {y,0} 中)和 Sum(在 {x,0} 中)的总和,并将结果存储在 {x,0} 中。

{deallocate,1}.

在准备从函数返回时,deallocate 指令会删除 allocate 创建的堆栈帧。

return.

return 从函数返回。返回值在 {x,0} 中。

这是该函数的 SSA 代码

function blog:foo(_0, _1) {
0:
  %% blog.erl:5
  _2 = call remote (literal lists):(literal sum)/1, _1

  %% blog.erl:5
  _3 = bif:'+' _0, _2
  @ssa_bool = succeeded _3
  br @ssa_bool, label 3, label 1

3:
  ret _3

1:
  @ssa_ret = call remote (literal erlang):(literal error)/1, literal badarg
  ret @ssa_ret
}

运行 beam_ssa_pre_codegen 后,SSA 代码如下所示

function blog:foo(x0/_0, x1/_1) {
  %% _0: 0..1
  %% _1: 0..1 0..3
%% #{frame_size => 1,yregs => [0]}
0:
  %% _0:4: 1..5
  [1] y0/_0:4 = copy x0/_0

  %% blog.erl:5
  %% _2: 3..5
  [3] x0/_2 = call remote (literal lists):(literal sum)/1, x1/_1

  %% blog.erl:5
  %% _3: 5..11
  [5] x0/_3 = bif:'+' y0/_0:4, x0/_2

  %% @ssa_bool: 7..9
  [7] z0/@ssa_bool = succeeded x0/_3
  [9] br z0/@ssa_bool, label 3, label 1

3:
  [11] ret x0/_3

1:
  %% @ssa_ret: 13..15
  [13] x0/@ssa_ret = call remote (literal erlang):(literal error)/1, literal badarg
  [15] ret x0/@ssa_ret
}

我们将描述 beam_ssa_pre_codegen 的重要(对于此示例)子 pass 的作用,并在这样做时指向代码的相关部分。

子 pass place_frames 确定应在何处分配堆栈帧。在此示例中,块 0 需要一个堆栈帧。

子 pass find_yregs 确定要放置在 Y 寄存器中的变量。结果将是一个添加到每个分配堆栈帧的块的 yregs 注释。对于此示例,注释如下所示

    %% #{frame_size => 1,yregs => [0]}

变量 _0 是 Erlang 代码中的 C。它需要在调用 lists:sum/1 时保存。

子 pass reserve_yregs 使用 yregs 注释并插入 copy 指令,以将需要保存的每个变量复制到新变量。对于该示例,将添加以下指令

  [1] y0/_0:4 = copy x0/_0

它将 _0 的值复制到 _0:4

子 pass number_instructions 对所有指令进行编号,以准备进行寄存器分配。在列表中,这些数字在每个指令之前的方括号中:[1][3][5] 等。

子 pass live_intervals 计算每个变量处于活动状态的时间间隔。在列表中,活动时间间隔显示为变量定义之前的注释

  %% _0:4: 1..5
  [1] y0/_0:4 = copy x0/_0

变量 _0:4 从指令 [1](其定义)到 [5](其最后一次使用)处于活动状态。

子 pass linear_scan 使用 线性扫描 算法为每个变量分配寄存器。结果将作为函数的注释保存。在 SSA 代码的列表中,寄存器将添加到变量的定义和每次使用中。例如

  [1] y0/_0:4 = copy x0/_0

变量 _0 (参数 L) 在 {x,0} 中。它在 _0:4 中的副本在 {y,0} 中。

但是,z0 是什么?

      [7] z0/@ssa_bool = succeeded x0/_3
      [9] br z0/@ssa_bool, label 3, label 1

succeeded 不是一个 BEAM 指令。它将与之前的指令(本例中为 bif:+)以及紧随其后的 br 指令合并到下一个 BEAM 指令中。

{gc_bif,'+',{f,0},1,[{y,0},{x,0}],{x,0}}.

因此,值 @ssa_bool 永远不会显式地存储在 BEAM 寄存器中。在我发明 Z 寄存器之前,@ssa_bool 会被分配给一个 X 寄存器。这在大多数情况下都有效,但有时一个 X 寄存器似乎被占用,即使它没有被占用,从而阻止另一个指令使用该寄存器。

以下是我在实现线性扫描时使用的参考资料

子过程 frame_size 使用线性扫描过程中的信息来计算每个堆栈帧的大小。结果存储为注释。

    %% #{frame_size => 1,yregs => [0]}

beam_ssa_codegen #

beam_ssa_codegen 过程从带注释的 SSA 代码生成 BEAM 代码。这个过程的测试更容易,因为我可以编译一些示例代码并尝试运行它。

通常,我甚至不需要运行代码就知道它是错误的。编译器会大声地告诉我。

blog: function bar/2+4:
  Internal consistency check failed - please report this bug.
  Instruction: {test_heap,2,3}
  Error:       {{x,2},not_live}:

现在是时候介绍 beam_validator 过程了。

beam_validator #

beam_validator 过程是在 R10B 的某个版本(可能是在 2006 年)中引入的。它在 BEAM 代码被打包成二进制文件并写入 BEAM 文件之前直接运行。beam_validator 的目的是找到可能导致运行时系统崩溃或以其他方式导致其行为不正常的非安全指令。

让我们看一个简单的例子。

bar(H, T) ->
    [H|T].

这是 BEAM 代码,但我对其进行了编辑,使其包含一个非安全指令。

      {label,4}.
        {test_heap,2,3}.
        {put_list,{x,0},{x,1},{x,0}}.
        return.

这里活动的寄存器数被给定为 3 而不是 2。这意味着 {x,0}{x,1}{x,2} 应该包含有效的 Erlang 项。因为 bar/2 只被调用时带有两个参数,所以 {x,2} 可以包含任何旧的垃圾。

在运行此代码时,它可能会导致运行时系统崩溃,或者它可能是完全无害的。这取决于在执行 test_heap 指令期间是否会发生垃圾回收,以及 {x,2} 中垃圾的确切性质。例如,如果垃圾碰巧是一个原子,那么什么坏事都不会发生。这意味着这种类型的编译器错误很难在测试用例中可靠地捕获。

beam_validator 会立即发现这个错误。它会跟踪函数中任何时刻哪些寄存器被初始化。如果它发现对未初始化的寄存器的引用,它会报错。

blog: function bar/2+4:
  Internal consistency check failed - please report this bug.
  Instruction: {test_heap,2,3}
  Error:       {{x,2},not_live}:

朋友和敌人 #

在实现 beam_ssa_codegen 期间,beam_validator 过程为我指出了许多错误。它是我的朋友。

它也算是我的敌人。它会抱怨一些完全安全的代码是不安全的。当这种情况发生时,我必须彻底检查代码以确保它是安全的,然后扩展 beam_validator 使其更智能,以便它理解代码是安全的。

这里有一个例子,我必须让 beam_validator 更智能。考虑以下代码:

{move,{x,0},{y,0}}.
{test,is_map,{f,777},[{x,0}]}.
{put_map_assoc,{f,0},{y,0},...}.

move 指令将 {x,0} 的副本存储在 {y,0} 中(堆栈上的一个位置)。接下来的 test 指令测试 {x,0} 是否是 map,如果不是,则分支到标签 777。put_map_assoc 指令更新 {y,0} 中的 map。

如果 put_map_assoc 指令的源参数不是 map,则它将崩溃。因此,如果 put_map_assoc 使用的源参数不是 map,则 beam_validator 会抱怨。在此示例中,beam_validator 没有看到确保 {y,0} 是 map 的 test 指令,因此它报错了。对于人类来说,很明显 {y,0} 是一个 map,因为它是一个 {x,0} 的副本,而 {x,0} 是一个 map。

v3_codegen 从未生成过这样的代码;事实上,它明确地避免生成此类代码。我不想在新代码生成器中添加类似的补丁,所以beam_validator 必须变得更智能

不安全的优化过程 #

beam_validator 发现的一些不安全代码确实是不安全的,但这并不是我的新编译器过程的错,而是优化过程优化生成的 BEAM 代码的错。

问题是,一些优化过程对 v3_codegen 会生成(或者说,会生成)的代码类型有隐式假设。新的代码生成器打破了这些假设。

起初,当我看到这些错误时,我删除了优化过程的损坏部分。使优化安全将是非平凡的,并且最终会浪费工作,因为我们打算重写所有这些优化过程以处理 SSA 代码。

当我看到太多这些不安全的优化时,我删除了所有不安全的优化过程

这意味着我们必须重新实现所有优化,生成的代码才能像旧编译器生成的代码一样好。我还注意到,新的 BEAM 代码生成器在某些方面比旧的生成器生成更好的代码,但在其他方面,代码更糟。例如,生成的代码使用了更多的堆栈空间,并进行了大量的寄存器洗牌。最终,必须以某种方式解决这个问题。

与此同时,我还有更糟糕的问题需要担心。

2018 年 3 月 #

3 月 14 日,我向 OTP 团队展示了我的新编译器过程的进展。我的一个幻灯片上有以下文字:

  • 可以编译 OTP 中的所有模块(并正确运行其中的许多模块)

是的,我已经完成了 beam_ssa_codegen 的初始实现,以便我可以编译 OTP 中的所有代码。

我在幻灯片中只是暗示的问题是,在运行测试套件时,Erlang 可能会崩溃并转储核心。不是每次都这样,而且从来没有在同一个测试用例中发生两次。只有当我使用新编译器编译 OTP 时才会发生这种情况。

崩溃似乎与测试用例本身无关,而是与日志文件的写入有关。我很快就确定,如果 file_io_server 已使用新的编译器过程编译,则可能会发生崩溃。然而,这并没有太大的帮助。该模块包含使用二进制语法、try/catchreceive 的复杂代码,所有这些都是复杂的指令,新编译器过程可能无法正确翻译。

beam_validator 应该在这些类型的错误导致崩溃之前捕获它们。要么是 beam_validator 没有查找的某种错误,要么是 BEAM 解释器中某些指令的实现存在错误。

我最终花费了整个三月份来尝试找出那个错误。

2018 年 4 月 #

在四月初,这个错误仍然让我难以捉摸。我已经稍微缩小了范围。我非常肯定这与 receive 有关。

然后,Rickard 给了我一些信息,我可以将其与我在查找错误期间吸收的另一条信息联系起来。

这个错误 #

本节内容有点高级,如果您愿意,可以跳到修复部分。

阅读有关Erlang 垃圾收集器的内容可以为更好地理解本节提供一些背景知识。

Rickard 提醒我注意 message_queue_data 选项,该选项已添加到 OTP 19 中的 process_flag/2 中。在调用 process_flag(message_queue_data, off_heap) 之后,所有尚未接收的消息都将存储在进程堆之外。将消息存储在堆之外意味着垃圾收集器不必在垃圾收集期间花费时间复制未接收的消息,这对于消息队列中有很多消息的进程来说是一个巨大的优势。

堆外消息的实现细节至关重要。考虑这个选择性 receive

receive
    {tagged_message,Message} -> Message
end.

当 BEAM 解释器执行此代码时,它将从外部消息队列中检索引用消息,并将其与元组模式进行匹配。如果消息不匹配,则将以相同的方式处理下一条消息,依此类推。

如果消息不匹配,则绝对不能在堆栈上存储对它的任何剩余引用。原因是,如果进行垃圾收集,垃圾收集器会将消息(或消息的一部分)复制到堆,更糟糕的是,它会在复制操作期间销毁原始消息。该消息仍在外部消息队列中,但它已被垃圾收集器损坏。如果该消息稍后在 receive 中匹配出来,则很可能会导致崩溃。

当 Rickard 首次实现堆外消息时,他问我编译器是否有可能在堆栈上存储对未接收消息的引用。我向他保证,这种情况不会发生。

是的,这是真的,这种情况不会发生,因为 v3_codegen 生成 receive 代码的方式。

有了新的编译器通道,可能会发生这种情况。我三月份第一次和 Richard 讨论这个 bug 时,他提到过禁止在堆栈上存储指向堆外消息的引用。当时,我并不知道编译器会在堆栈上存储指向堆外消息的引用。

四月份,当 Rickard 第二次提醒我这件事时,我记得在我的 bug 搜索期间看到生成的代码在堆栈上存储了堆外消息引用。

修复方案 #

在找到 bug 的原因后,我首先教 beam_validator 抱怨堆栈上的“脆弱引用”。我将该提交包含在了 OTP 21 中。

然后,我在 beam_ssa_pre_codegen 中添加了一个子通道来重写 receive。它引入了新的变量和 copy 指令,以确保任何对正在匹配的消息的引用都保留在 X 寄存器中。

在代码生成器中没有已知 bug 的情况下,我可以开始重写我之前删除的优化通道了。

更多优化 #

beam_ssa_recv #

beam_ssa_recv 是不安全的 beam_receive 通道的替代品。其目的是优化只能匹配新创建的引用的 receive。此优化避免扫描在创建引用之前放入消息队列中的消息。

实际上,我是在三月初编写 beam_ssa_recv 的,作为一项实验,看看编写 SSA 代码的优化有多容易。结果证明非常容易。 beam_ssa_recv 可以比 beam_receive 在更多地方应用优化,并且使用更少的代码。

在旧的 beam_receive 通道中,需要大量的代码来处理 BEAM 指令的多种变体。例如,在 opt_update_regs/3 中,有三个子句仅用于处理 call 指令的三种变体(调用局部函数、调用外部函数和调用 fun)。

这是一个 beam_receive 没有优化但 beam_ssa_recv 可以优化的函数示例

beam_ssa_opt #

beam_ssa_opt 通道运行多项优化。许多优化是我先前删除的优化的替代方案。

beam_ssa_type #

beam_ssa_type 替换了不安全的 beam_type 通道。

beam_type 通道进行局部类型分析(基本上是针对扩展的基本块),并尝试简化代码,例如删除不必要的类型测试。

beam_ssa_type 通道分析整个函数中的类型并简化代码,例如删除不必要的类型测试。它比 beam_type 发现更多的优化机会。

2018 年 5 月 #

在 5 月初,John 开始开发后来成为此 PR 的内容

#1958:在新版基于 SSA 的中间格式中重写 BSM 优化

我继续编写优化代码,并修复 John 在开发他的优化代码时发现的 bug。

重新思考二进制匹配指令 #

在开发他的二进制优化时,John 意识到用于二进制匹配的 SSA 指令难以优化。我设计的二进制匹配指令与 BEAM 指令的语义非常接近。John 建议将 bs_get 指令分解为 bs_match 指令和 bs_extract 指令,以简化优化。

指令的分解意味着 beam_ssa_pre_codegen 将必须更加努力地将它们组合起来,但这大大简化了 John 的优化。事实证明,它还启用了其他优化:活跃性优化可以更积极地删除未使用的指令。

在 5 月 31 日的 Code BEAM STO 2018 的第一天,我不知道新的编译器通道中有任何 bug,并且我需要重新实现的优化列表正在稳步缩减。我在那里遇到了 Michał Muskała(Erlang/OTP 的常客贡献者和 Elixir 核心团队 的成员),并告诉他我关于编译器的工作,以及它已经足够稳定,可以在 OTP 之外进行测试,例如编译 Elixir 代码……

2018 年 6 月 #

Michał 的反馈 #

我在六月中旬收到了 Michał 的电子邮件。他试用了我的编译器分支。他写道

第一印象是,编译 Elixir 的 unicode 模块花费了很长时间,如此之久以至于我不得不在大约 10 分钟后将其关闭。

他向我发送了 Elixir 的 unicode 模块的 Erlang 化版本。该模块的 Erlang 源代码大小接近 82,000 行,大约 3,700,000 字节。基于大小,可以预期编译速度会稍微慢一些,但不会那么慢。在他的计算机上,OTP 21.0-RC2 在 16 秒内完成了编译。

我使用 time 选项编译了该模块。最慢的通道是 beam_ssa_type。经过进一步的分析,我发现在两个 map 的连接中存在瓶颈。这是已更正的代码。原始代码没有比较 map 的大小并根据需要交换它们。我也可能做了一些其他的改进。无论如何,这解决了那个瓶颈。现在 beam_ssa_pre_codegen 是最慢的通道。

我修复了 线性扫描子通道中的几个瓶颈,此后又修复了 beam_ssa_pre_codegen 中的一些其他瓶颈。我认为这使编译时间减少到一分钟以下。

优化代码生成 #

在完成最后一个优化通道的重新实现(我认为这是浮点运算的优化,就像之前由不安全的 beam_type 通道所做的那样)之后,我开始比较 OTP 21 生成的代码和来自新编译器通道的代码。

我使用了 scripts/diffable,它将 OTP 中的大约 1000 个模块编译为 BEAM 代码,并对 BEAM 代码进行处理,使其更便于进行 diff。然后,我运行了 diff -u old new 来比较新代码和旧代码。

在六月的最后一部分和七月的第一周,我改进了 beam_ssa_pre_codegenbeam_ssa_codegen,以解决我在读取 diff 时注意到的问题。

beam_ssa_pre_codegen 的改进 #

我没有更改 beam_ssa_pre_codegen 本身的 线性扫描子通道。相反,我添加了 SSA 代码的转换,这将有助于线性扫描更好地分配寄存器。

我注意到的最明显的问题是不必要的 move 指令。以下是我为解决该问题而添加的两个子通道

  • reserve_xregs 向线性扫描子通道提供提示,如果可能,某个 X 寄存器应该用于某个变量。

  • opt_get_list 尝试消除从列表中匹配出元素时经常添加的额外 move 指令。有关示例和解释,请参阅代码中的注释。

另一个常见问题是,从新代码生成器生成的代码使用了更多的堆栈空间,因为两个并非严格同时活跃的变量被分配了不同的 Y 寄存器(堆栈上的槽),而不是重用同一个 Y 寄存器。我在 copy_retval 中解决了这个问题。有关示例,请参阅代码中的注释。

beam_ssa_codegen 的改进 #

Michał 注意到,当一个值同时存储在 X 寄存器和 Y 寄存器(堆栈上)中时,使用该值的指令总是会使用 Y 寄存器。旧的代码生成器将使用 X 寄存器。由于 BEAM 解释器通常针对 X 寄存器中的操作数进行了优化,因此新代码可能会更慢。

我添加了 prefer_xregs 来解决这个问题。有关示例,请参阅代码中的注释。

2018 年 7 月 #

休假。

2018 年 8 月 #

在我休假之前,看起来新的编译器通道通常会生成至少与旧编译器通道一样好的代码。在某些情况下,代码会好得多。

休假回来后,我做了一些最后的润色。

8 月 17 日,我创建了一个 PR

在合并 PR 之前,我偷偷进行了一些最后的优化。

8 月 24 日,我 合并了 该 PR。

未来 #

基于 SSA 的中间表示为编译器未来的改进提供了坚实的基础。在八月份合并 PR 后,已经有多个 PR 添加了进一步的改进

以下是在 OTP 22 发布之前可以由 OTP 成员或外部贡献者实现的可能的进一步改进列表

  • 重写 sys_core_dsetel 为基于 SSA 的代码。

  • v3_kernel 中的保护优化子通道 guard_opt/2 重写为基于 SSA 的优化通道。

  • 重写 beam_trim。它可能必须是 beam_ssa_codegen 的一部分。

  • 优化 switch 分支。如果两个分支跳转到做相同事情的块,则让两个分支都跳转到相同的块。 beam_jump 会执行这种优化,但在 SSA 表示中更早地进行此操作可以加快具有多个子句的函数的编译速度。

  • 移除 beam_utils 模块,特别是 is_killed()is_not_used() 系列函数。beam_jump 使用的 beam_utils 中的函数可以移到 beam_jump 中。

  • 重写 beam_bs 以基于 SSA(静态单赋值)。这种重写可能不会改善生成的代码,但可能会加快大量使用二进制语法的模块的编译速度。