核心 Erlang 总结

2018 年 5 月 30 日 · 作者:Björn Gustavsson

这篇博文总结了之前两篇博文中开始的对核心 Erlang 的探索。接下来将介绍剩余的默认核心 Erlang 传递,然后看看核心 Erlang 在编译器内部是如何表示的。

以下是在使用 OTP 21 RC1 或 git 存储库中的 master 分支时将运行的核心 Erlang 传递

$ erlc +time core_wrapup.erl
Compiling "core_wrapup"
     .
     .
     .
 core                          :      0.000 s      15.7 kB
 sys_core_fold                 :      0.000 s       9.0 kB
 sys_core_alias                :      0.000 s       9.0 kB
 core_transforms               :      0.000 s       9.0 kB
 sys_core_bsm                  :      0.000 s       9.0 kB
 sys_core_dsetel               :      0.000 s       9.0 kB
     .
     .
     .

在之前关于核心 Erlang 的两篇博文中,我们已经介绍了 coresys_core_fold

其他核心 Erlang 传递 #

sys_core_alias #

在即将发布的 OTP 21 版本中,有一个由 José Valim 贡献的新的 sys_core_alias 传递。

此传递的目的是避免重建已匹配的项,例如在以下示例中

remove_even([{Key,Val}|T]) ->
    case Val rem 2 =:= 0 of
        true -> remove_even(T);
        false ->  [{Key,Val}|remove_even(T)]
    end;
remove_even([]) -> [].

在函数头中,模式 {Key,Val} 将元组的两个元素绑定到变量 KeyVal,但原始元组未被捕获。在 casefalse 子句中,将从 KeyVal 构建一个新的元组。

可以通过使用 = 运算符将完整元组绑定到变量来避免创建新的元组

remove_even([{Key,Val}=Tuple|T]) ->
    case Val rem 2 =:= 0 of
        true -> remove_even(T);
        false ->  [Tuple|remove_even(T)]
    end;
remove_even([]) -> [].

本质上,新的 sys_core_alias 传递会自动执行该转换。这是应用此优化之前的核心 Erlang 代码

'remove_even'/1 =
    fun (_0) ->
	case _0 of
	  <[{Key,Val}|T]> when 'true' ->
	      let <_1> =
		  call
		       'erlang':'rem'(Val, 2)
	      in
		  case <> of
		    <>
			when call 'erlang':'=:='(_1, 0) ->
			    apply 'remove_even'/1(T)
		    <> when 'true' ->
			let <_2> =
			    apply 'remove_even'/1(T)
			in
                            [{Key,Val}|_2]      % BUILDING TUPLE
		  end
	  <[]> when 'true' ->
	      []
	  <_4> when 'true' ->
		primop 'match_fail'({'function_clause',_4})
	end

这是运行 sys_core_alias 传递之后的代码

'remove_even'/1 =
    fun (_0) ->
	case _0 of
	  <[_@r0 = {Key,Val}|T]> when 'true' ->
	      let <_1> =
		  call 'erlang':'rem'(Val, 2)
	      in
		  case <> of
		    <>
			when call 'erlang':'=:='(_1, 0) ->
			    apply 'remove_even'/1(T)
		    <> when 'true' ->
			let <_2> =
			    apply 'remove_even'/1(T)
			in
			    [_@r0|_2]          % REUSING EXISTING TUPLE
		  end
	  <[]> when 'true' ->
	      []
	  <_4> when 'true' ->
		primop 'match_fail'({'function_clause',_4})
	end

core_transforms #

与解析转换类似,core_transforms 传递可以添加编译器传递,从而转换核心 Erlang 代码而无需修改编译器。

例如,这是一个简单的核心转换模块

-module(my_core_transform).
-export([core_transform/2]).

core_transform(Core, _Options) ->
    Module = cerl:concrete(cerl:module_name(Core)),
    io:format("Module name: ~p\n", [Module]),
    io:format("Number of nodes in Core Erlang tree: ~p\n",
              [cerl_trees:size(Core)]),
    Core.

在解释代码之前,让我们看看它的实际效果

$ erlc my_core_transform
$ erlc -pa . '+{core_transform,my_core_transform}' core_wrapup.erl
Module name: core_wrapup
Number of nodes in Core Erlang tree: 220
$

{core_transform,Name} 选项指示编译器运行核心转换。在本例中,核心转换模块是 my_core_transform。在完成标准优化传递后,编译器将调用 my_core_transform:core_transform/2,将核心 Erlang 代码作为第一个参数,将编译器选项作为第二个参数传递。

core_transform/2 函数中的第一行调用 cerl:module_name(Core) 来检索模块名称。cerl:module_name/1 的返回值是一个记录,表示任何文字项。要检索实际的项(在本例中为原子),将调用 cerl:concrete/1

在第二个 io:format/2 调用中,我们调用 cerl_trees:size/1 来计算表示模块核心 Erlang 代码的树中的节点数。

此核心转换不执行任何实际转换,因为最后一行返回核心 Erlang 代码而没有任何修改。

sys_core_bsm #

sys_core_bsm 是实现效率指南中描述的延迟子二进制优化的三个传递中的第一个。sys_core_bsm 添加注释,这些注释稍后被 v3_codegenbeam_bsm 用于优化二进制文件的匹配。

sys_core_dsetel #

sys_core_dsetel 传递将优化 setelement/3 的链式或嵌套应用程序,如以下示例中所示

update_tuple(T0) ->
    T = setelement(3, T0, y),
    setelement(2, T, x).

翻译成核心 Erlang 后,如下所示

'update_tuple'/1 =
    fun (_0) ->
	let <T> =
	    call 'erlang':'setelement'(3, _0, 'y')
	in
	    call 'erlang':'setelement'(2, T, 'x')

sys_core_dsetel 传递将对 setelement/3 的第二次调用替换为原语 dsetelement/3,该原语会破坏性地更新元组

'update_tuple'/1 =
    fun (_0) ->
	let <T> =
	    call 'erlang':'setelement'(3, _0, 'y')
	in  do
		primop 'dsetelement'(2, T, 'x')
		T

do 按顺序计算两个表达式,忽略第一个表达式的值。它在这里使用是因为原语 dsetelement/3 更新其元组参数而不返回值。

sys_core_dsetel 传递有意作为最后一个核心 Erlang 传递运行。进行其他优化可能会使优化不安全。例如,在调用 setelement/3dsetelement/3 之间不得发生垃圾回收。

为什么这种优化有用?setelement/3 调用的序列肯定很少见?

考虑这个更新记录中两个元素的函数

-record(rec, {a,b,c,d,e,f,g,h}).

update_record(R) ->
    R#rec{a=x,b=y}.

之前的博文中,我们看到 -E 选项将生成一个 .E 文件,其中包含记录转换为元组操作后的代码

$ erlc -E core_wrapup.erl

这是记录转换后 update_record/1 的代码

update_record(R) ->
    begin
        rec0 = R,
        case rec0 of
            {rec,_,_,_,_,_,_,_,_} ->
                setelement(2, setelement(3, rec0, y), x);
            _ ->
                error({badrecord,rec})
        end
    end.

在验证 R 确实是正确类型的记录(即元组的大小和第一个元素正确)之后,使用嵌套的 setelement/3 调用来更新元组的两个元素。

update_record/1 的优化核心 Erlang 代码将如下所示

'update_record'/1 =
    fun (_0) ->
	case _0 of
	  <{'rec',_5,_6,_7,_8,_9,_10,_11,_12}> when 'true' ->
	      let <_2> =
		  call 'erlang':'setelement'(3, _0, 'y')
	      in  do  primop 'dsetelement'(2, _2, 'x')
		      _2
	  <_13> when 'true' ->
		call 'erlang':'error'({'badrecord','rec'})
	end

核心 Erlang 代码的表示 #

到目前为止,我们已经了解了核心 Erlang 的外部(美化打印)表示形式。在离开核心 Erlang 之前,我们将简要了解编译器使用的核心 Erlang 的内部表示形式。

在优化器传递中,有三种方法来处理核心 Erlang

  • 使用 cerl 模块中的 API 函数

  • 使用 core_parse.hrl 中定义的 c_* 记录

  • 将记录的使用与 API 函数的使用混合使用

使用 cerl 模块及其友元 #

cerl 模块提供了 API 函数来构造、解构、更新和查询核心 Erlang 中的每个构造。

以下是一些示例

  • cerl:c_var(Name) 构造一个名称为 Name 的变量的核心 Erlang 表示形式。

  • 如果 Core 表示核心 Erlang 变量,则 cerl:is_c_var(Core) 返回 true,否则返回 false

  • cerl:var_name(Core) 返回变量的名称(如果 Core 不表示核心 Erlang 变量,则崩溃)。

还有 cerl_treescerl_clauses 模块,它们提供了用于操作核心 Erlang 代码的有用实用函数。

使用记录 #

core_parse.hrl 中,每种核心 Erlang 构造都有一个记录。所有记录名称都以 c_ 前缀开头。

例如,记录 #c_var{} 表示一个变量,记录 #c_call{} 表示 call 表达式,记录 c_tuple{} 表示一个元组,依此类推。

作为一个完整的示例,我们可以重写之前的核心转换,以使用记录匹配而不是 cerl 来检索模块名称

-module(my_core_transform).
-export([core_transform/2]).

-include_lib("compiler/src/core_parse.hrl").

core_transform(Core, _Options) ->
    #c_module{name=#c_literal{val=Module}} = Core,
    io:format("Module name: ~p\n", [Module]),
    io:format("Number of nodes in Core Erlang tree: ~p\n",
              [cerl_trees:size(Core)]),
    Core.

cerl API 与记录混合使用 #

cerl 模块在内部使用 core_parse.hrl 中的记录,因此可以将这两种方法混合使用。例如,sys_core_fold 主要使用记录,但有时在更方便时使用 cerl

总结总结 #

似乎有足够的材料可以再写几篇关于核心 Erlang 的博文。例如,我甚至没有提到内联器(不是笔误,有两个内联器)。这意味着将来可能会有更多关于核心 Erlang 的博文。

但在不久的将来,是时候探索核心 Erlang 之后的编译器传递,也许可以回答关于 v3_ 前缀的永恒问题。曾经有过 v2_kernel(剧透:是的)或 v1_kernel(剧透:没有)吗?