《Erlang虚拟机(VM)简介》一文中介绍的寄存器和指令参数类型是学习Erlang VM相关知识的基础,也是理解本文内容的前提,这里就不赘述了。
先看将要分析的实例的Erlang源代码:
erlc +"'S'" test.erl
生成汇编文件:
注1:很多函数调用的指令后面都会有一个数字,不太理解这个数字是干什么用的。Google了一会,没找到答案。可能它是一个地址值,作为被调用函数的运行空间的起始地址。但这只是一个猜测,即使是这样,这个值又是怎么样计算出来的?
现以加法为例查找一下这个数字的来源。
在ops.tab中查找gc_bif,有如下结果:
再从beam_load.c中找到如下结果:
被调用的函数只能使用Live值以上的寄存器,这样就不会覆盖正在使用的寄存器值,使用这种方法,也省去了使用栈。但是,这种方法不会导致寄存器不够用吗?或许只有简单函数的调用才做了这样的优化。
先看将要分析的实例的Erlang源代码:
%% mytest.erl
-module(mytest).
-compile(export_all).
%% 返回atom
t1() ->
ok.
%% 调用BIF
t2(A) ->
integer_to_list(A).
%% 加法运算
t3(A) ->
A + 10.
%% 调用内部函数
t4(A) ->
t3(A).
%% 函数分支
t5(0) -> 0;
t5(A) -> A + 1.
%% 尾递归
t6([], Result) -> Result;
t6([H | T], Result) ->
H1 = H + 1,
t6(T, [H1 | Result]).
erlc +"'S'" test.erl
生成汇编文件:
%% test.S
{module, mytest}. %% version = 0
{exports, [{module_info,0},
{module_info,1},
{t1,0},
{t2,1},
{t3,1},
{t4,1},
{t5,1},
{t6,2}]}.
{attributes, []}.
{labels, 19}.
%% 返回atom
{function, t1, 0, 2}.
%% label就是标号,或者称为代码标签,
%% 在跳转时,这是一个程序的入口点
{label,1}.
{line,[{location,"mytest.erl",5}]}.
{func_info,{atom,mytest},{atom,t1},0}.
{label,2}.
%% move指令,将原子“ok”送寄存器0
{move,{atom,ok},{x,0}}.
%% 返回
return.
%% 调用BIF
%% t2(A) ->
%% integer_to_list(A).
{function, t2, 1, 4}.
{label,3}.
{line,[{location,"mytest.erl",9}]}.
{func_info,{atom,mytest},{atom,t2},1}.
{label,4}.
{line,[{location,"mytest.erl",10}]}.
%% 调用BIF integer_to_list
{call_ext_only,1,{extfunc,erlang,integer_to_list,1}}.
%% 加法运算
%% t3(A) ->
%% A + 10.
{function, t3, 1, 6}.
{label,5}.
{line,[{location,"mytest.erl",13}]}.
{func_info,{atom,mytest},{atom,t3},1}.
{label,6}.
{line,[{location,"mytest.erl",14}]}.
%% 调用Guard BIF,+操作符也是一个BIF函数,
%% 参数为[{x,0},{integer,10}],然后把结果送R0,即R0累加立即数10,
%% 如果运行出错,则跳转到label 0(这里是抛出错误)
%% {f,0}后面的1是干什么用的?(见注1)
{gc_bif,'+',{f,0},1,[{x,0},{integer,10}],{x,0}}.
return.
%% 调用内部函数
{function, t4, 1, 8}.
{label,7}.
{line,[{location,"mytest.erl",17}]}.
{func_info,{atom,mytest},{atom,t4},1}.
{label,8}.
%% 跳转到label 6,即调用函数t3
{call_only,1,{f,6}}.
%% 函数分支
%% t5(0) -> 0;
%% t5(A) -> A + 1.
{function, t5, 1, 10}.
{label,9}.
{line,[{location,"mytest.erl",21}]}.
{func_info,{atom,mytest},{atom,t5},1}.
{label,10}.
%% 测试R0是否等于0
{test,is_eq_exact,{f,11},[{x,0},{integer,0}]}.
%% 如果测试成功则返回,否则跳到label 11
return.
{label,11}.
{line,[{location,"mytest.erl",22}]}.
%% 执行加法运算
{gc_bif,'+',{f,0},1,[{x,0},{integer,1}],{x,0}}.
return.
%% 尾递归
%% t6([], Result) -> Result;
%% t6([H | T], Result) ->
%% H1 = H + 1,
%% t6(T, [H1 | Result]).
{function, t6, 2, 13}.
{label,12}.
{line,[{location,"mytest.erl",25}]}.
{func_info,{atom,mytest},{atom,t6},2}.
{label,13}.
%% 测试R0是否是一个非空list,如果不是则跳转到{label,14}
{test,is_nonempty_list,{f,14},[{x,0}]}.
%% R0保存的是一个list指针
%% 从list(R0)头部取出一个元素送R2,剩余的送R3,
%% 相当于:[H|T] = R0, R2 = H, R3 = T
{get_list,{x,0},{x,2},{x,3}}.
{line,[{location,"mytest.erl",27}]}.
%% R2 + 1 结果送 R0
%% {f,0}后面的4是干什么用的?(见注1)
{gc_bif,'+',{f,0},4,[{x,2},{integer,1}],{x,0}}.
%% 测试堆空间至少有2 words,4为Live
%% 这里传入的Live值有什么作用?为了GC吗?
{test_heap,2,4}.
%% HTOP[0] = R0, HTOP[1] = R1, R1 = &HTOP[0]
{put_list,{x,0},{x,1},{x,1}}.
%% R3送R0,R3即是上面剩余的list
{move,{x,3},{x,0}}.
%% 目前已经为下一次的调用准备好了R0和R1这两个参数
%% 调用自已{label,13}
%% 生成opcode时,call_only会合并上面的move一起生成move_call_only_xrf x(3) x(0) mytest:t6/2 ???
{call_only,2,{f,13}}.
{label,14}.
%% 测试R0是否为空list,如果不为空则跳转到{label,12}报错
{test,is_nil,{f,12},[{x,0}]}.
%% 将R1送R0,这是函数的最后返回结果
{move,{x,1},{x,0}}.
return.
%% 以下两个函数,你懂的,不解释。
{function, module_info, 0, 16}.
{label,15}.
{line,[]}.
{func_info,{atom,mytest},{atom,module_info},0}.
{label,16}.
{move,{atom,mytest},{x,0}}.
{line,[]}.
{call_ext_only,1,{extfunc,erlang,get_module_info,1}}.
{function, module_info, 1, 18}.
{label,17}.
{line,[]}.
{func_info,{atom,mytest},{atom,module_info},1}.
{label,18}.
{move,{x,0},{x,1}}.
{move,{atom,mytest},{x,0}}.
{line,[]}.
{call_ext_only,2,{extfunc,erlang,get_module_info,2}}.
注1:很多函数调用的指令后面都会有一个数字,不太理解这个数字是干什么用的。Google了一会,没找到答案。可能它是一个地址值,作为被调用函数的运行空间的起始地址。但这只是一个猜测,即使是这样,这个值又是怎么样计算出来的?
现以加法为例查找一下这个数字的来源。
在ops.tab中查找gc_bif,有如下结果:
#
# Optimize addition and subtraction of small literals using
# the i_increment/4 instruction (in bodies, not in guards).
#
gc_bif2 p Live u$bif:erlang:splus/2 Int=i Reg=d Dst => \
gen_increment(Reg, Int, Live, Dst)
gc_bif2 p Live u$bif:erlang:splus/2 Reg=d Int=i Dst => \
gen_increment(Reg, Int, Live, Dst)
再从beam_load.c中找到如下结果:
static GenOp* gen_increment(LoaderState* stp, GenOpArg Reg, GenOpArg Integer, GenOpArg Live, GenOpArg Dst) { GenOp* op; NEW_GENOP(stp, op); op->op = genop_i_increment_4; op->arity = 4; op->next = NULL; op->a[0] = Reg; op->a[1].type = TAG_u; op->a[1].val = Integer.val; op->a[2] = Live; // 找的就是你!!! op->a[3] = Dst; return op; }
原来它是一个Live值。
被调用的函数只能使用Live值以上的寄存器,这样就不会覆盖正在使用的寄存器值,使用这种方法,也省去了使用栈。但是,这种方法不会导致寄存器不够用吗?或许只有简单函数的调用才做了这样的优化。