Ruby 1.8.x中复合赋值运算符的实现
嗯,在RPGCHINA读帖的时候看到一个有趣的主题,说Ruby的a = a + 1与a += 1的执行效率不一样。很明显这个认识有偏差,事实上Ruby的复合赋值运算符与其展开的简单赋值形式在经过解释器前端的解析后就一模一样了。连对应的抽象语法树都是一样的,执行效率能差多少呢,
更糟糕的是回帖中有错误的解释,将这个“差异”对应到汇编上:
引用
引用第8楼nightaway于2008-03-12 21:33发表的 :
a+=1 编译后的汇编指令 add
a++ 编译后的汇编指令 inc
inc 的运算周期 小于 add 所以 a++ 比 a+=1 快
如果楼主有疑问可以学学汇编语言.. 这样你的编码水平会大增..
这不是答非所问么„„那帖的主题明明是在讨论a += 1与a = a + 1的差异;+=与++又不等价,况且Ruby里连++运算符都没有 =_=|||
引用
引用第12楼jiangcq于2008-05-26 10:32发表的 :
回答正确,+= 比 = + 效率要高很多,特别是在早期的计算机上
####################
a=a+1
MOV EAX,A
MOV EBX,1
ADD EAX,EBX
MOV A,EAX
##################
a+=1
MOV EAX,A
INC EAX
MOV A,EAX
####################
如果没记错的话应该是这样计算的
诶,所以说不知道就不要乱说嗯。这MOV EBX, 1在运算支持立即数的指令集里明显是废的。
编译器能做的优化多的是。偏偏这a = a + 1却没什么优化可做。假如这是C代码生成为x86的目标代码,编译器选择ADD r32, #1还是INC r32与这个语句写成a = a + 1还是a += 1没什么关系„„要说跟a++或者++a扯上关系那还靠谱点,不过如果是好的优化编译器都应该能生成一样的代码。
这复合赋值运算符最大的意义在于:1、让源代码更加简洁易读;2、减少重复的地址计算。之所以a = a + 1与a += 1无论如何也差不了多少是因为a本身就已经是一个变量了,能“直接”访问。假如这是一个需要昂贵的地址计算的表达式,那复合赋值运算符就显得很有意义了。例如这样:
a.prop[0][1] = a.prop[0][1] + 1;
这里要赋值的目标的地址无法直接得到,得经过复杂的计算。这种情况下取值和赋值分开两次来计算地址就比只计算一次要慢一些,因而有优化的必要。
原帖地址:
要讨论这种问题果然还是得研究一下MRI到底是如何实现这些东西的。
YARV的实现机制比较不同,而且1.9.0这个实验系列何时才会进化到稳定版的2.0.x还很难说,所以还是拿1.8.x系列为准来讨论了。
===========================================================================
下面内容是我在那帖14楼的回复:
这个„„事情要具体问题具体
对吧,张冠李戴就不太好了 ^ ^
+=之类的复合赋值运算符在许多语言都有,语义类似但是实现的方式并不总相同。
RGSS里的脚本语言是Ruby,RPG Maker VX里使用的Ruby是1.8.1版的MRI。Ruby源代码并没有被直接编译到机器码,而是被Ruby解释器所解释:先把源代码翻译成抽象语法树,然后解释抽象语法树。
在Ruby里,一切皆是对象。因此像是加号减号之类的运算,也被看作是对象上的方法。a += 1的语义是a = a.+(1)(语义是:调用a对象上的+()方法,把1作为参数传进去,然后将方法的返回值赋值给a。更准确的说,是对右手边的a对象发送了一个";+";消息,以1为参数;返回得到的值赋值给左手边的a)。
+=的语义不是单独定义,而是由+()方法所决定的;换句话说一个类定义了+()方法就自动具备了+=。假如有语句a = 1,那么a是一个Fixnum,+=当中调用的+()方法就是Fixnum#+()。
先看看";+=";这个符号被解析器识别为什么了。Ruby的扫描器(词法分析器)里有这么一段:
parse.y
case '+':
c = nextc();
if (lex_state == EXPR_FNAME || lex_state == EXPR_DOT) {
lex_state = EXPR_ARG;
if (c == '@') {
return tUPLUS;
}
pushback(c);
return '+';
}
if (c == '=') {
yylval.id = '+'; // 注意这里,id是'+'
lex_state = EXPR_BEG;
return tOP_ASGN; // 然后整体以tOP_ASGN返回
}
可以看到+=被识别为tOP_ASGN类型的token。
a += 1形式的语句对应的这条语法:
statement: //...
| var_lhs tOP_ASGN command_call
// ...
| //...
;
语法对应着解析器(语法分析器),而解析器会生成抽象语法树。如果等号前的是||则语法生成NODE_OP_ASGN_OR节点,如果是&;&;则生成NODE_OP_ASGN_AND节点,其它则调用call_op()
函数生成NODE_CALL节点。
parse.y
var_lhs tOP_ASGN command_call {
value_expr($3);
if ($1) {
ID vid = $1->;nd_vid;
if ($2 == tOROP) {
$1->;nd_value = $3;
$$ = NEW_OP_ASGN_OR(gettable(vid), $1);
if (is_asgn_or_id(vid)) {
$$->;nd_aid = vid;
}
}
else if ($2 == tANDOP) {
$1->;nd_value = $3;
$$ = NEW_OP_ASGN_AND(gettable(vid), $1);
}
else {
$$ = $1; // 获得var_lhs对应的节点
// call_op将返回一个NODE_CALL节点,并赋值给var_lhs对应节点的“值”
$$->;nd_value = call_op(gettable(vid),$2,1,$3);
}
}
else {
$$ = 0;
}
}
由于节点的“值”(nd_value)被赋值为一个NODE_CALL节点,这里实质上完成了将a += 1
变为a = a.+(1)的转换。
看看a = a + 1对应的语法和动作:
parse.y
lhs '=' command_call
{
$$ = node_assign($1, $3); }
结合下面node_assign()函数的实现,可以看到这里是把右手边的节点赋值给了左手边节点的“值”(nd_value)。并且,右手边的a + 1对应的语法与动作如下:
parse.y
arg '+' arg
{
$$ = call_op($1, '+', 1, $3); }
也是调用call_op()生成NODE_CALL节点,跟前面a += 1时一样。
于是,a += 1与a = a + 1在被解析后所生成的语法树是一样的,后续执行中就都是等价的
了。
parse.y
static NODE*
node_assign(lhs, rhs)
NODE *lhs, *rhs;
{
if (!lhs) return 0;
value_expr(rhs);
switch (nd_type(lhs)) {
case NODE_GASGN:
case NODE_IASGN:
case NODE_LASGN:
case NODE_DASGN:
case NODE_DASGN_CURR:
case NODE_MASGN:
case NODE_CDECL:
case NODE_CVDECL:
case NODE_CVASGN:
lhs->;nd_value = rhs; // 注意这里
break;
case NODE_ATTRASGN:
case NODE_CALL:
lhs->;nd_args = arg_add(lhs->;nd_args, rhs);
break;
default:
/* should not happen */
break;
}
return lhs;
}
(nd_value是在node.h里定义的一个宏,展开为u2.node)
===========================================================================
这是Fixnum#+()对应的C函数:
numeric.c
/*
* call-seq:
* fix + numeric =>; numeric_result
*
* Performs addition: the class of the resulting object depends on
* the class of <;code>;numeric<;/code>; and on the magnitude of the
* result.
*/
static VALUE
fix_plus(x, y)
VALUE x, y;
{
if (FIXNUM_P(y)) {
long a, b, c;
VALUE r;
a = FIX2LONG(x);
b = FIX2LONG(y);
c = a + b;
r = LONG2NUM(c);
return r;
}
if (TYPE(y) == T_FLOAT) {
return rb_float_new((double)FIX2LONG(x) + RFLOAT(y)->;value);
}
return rb_num_coerce_bin(x, y); }
该函数被注册到Ruby的类型系统中:
numeric.c
rb_define_method(rb_cFixnum, ";+";, fix_plus, 1);
rb_cFixnum是Ruby的Fixnum的C的实现类,继承自rb_cInteger:
numeric.c
rb_cFixnum = rb_define_class(";Fixnum";, rb_cInteger);
上面rb_define_method函数使得fix_plus与一个NODE_CFUNC关联在了一起。这个函数会调用rb_intern(name)来将方法名转换为ID,这里对运算符做了特殊处理:
parse.y
if (name[0] != '_' &;&; ISASCII(name[0]) &;&; !ISALNUM(name[0])) {
/* operators */
int i;
for (i=0; op_tbl[i].token; i++) {
if (*op_tbl[i].name == *name &;&;
strcmp(op_tbl[i].name, name) == 0) {
id = op_tbl[i].token;
goto id_regist;
}
}
}
这个特殊处理可以保证运算符与内建函数的对应关系。
P.S. 以上代码来自Ruby 1.8.7的源码。
P.P.S Ruby Hacking Guide真是本好