核心 Erlang 总结
这篇博文总结了之前两篇博文中开始的对核心 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 的两篇博文中,我们已经介绍了 core
和 sys_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}
将元组的两个元素绑定到变量 Key
和 Val
,但原始元组未被捕获。在 case
的 false
子句中,将从 Key
和 Val
构建一个新的元组。
可以通过使用 =
运算符将完整元组绑定到变量来避免创建新的元组
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_codegen
和 beam_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/3
和 dsetelement/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_trees
和 cerl_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
(剧透:没有)吗?