Ruby多重赋值的底层原理是什么?

例如a,b = b,a 可以一行就置换a和b的值 是否也是借用第三个变量进行交换?
关注者
23
被浏览
2850
题主的问题是:
Ruby多重赋值的底层原理是什么?
例如 a, b = b, a 可以一行就置换a和b的值
是否也是借用第三个变量进行交换?
这是关于Ruby的并行赋值(parallel assignment,或者叫multiple assignment,缩写massign)的问题。

语义上,Ruby的并行赋值表达式会保证对应位置的值的赋值效果都是“同时发生”的——赋值运算符右手边的操作数。所以不但可以用来实现2个变量的交换,还可以用来实现多个变量的任意顺序(包括交换、循环交换之类)的赋值。同时,并行赋值表达式自身也有值,其值为赋值运算符右手边的所有操作数为元素所构成的数组(或者如果右手边自身就是一个数组的话,直接以它为值)。
例如说:
$ irb
irb(main):001:0> a, b, c = 1, 2, 3
=> [1, 2, 3]
irb(main):002:0> d = (a, b, c = b, a, b)
=> [2, 1, 2]
irb(main):003:0> require 'pp'
=> true
irb(main):004:0> pp a, b, c, d; nil
2
1
2
[2, 1, 2]
=> nil
可以看到,对于 a, b, c = c, a, b 这个并行赋值表达式,它的效果是在赋值前把右手边操作数的值同时记录下来,然后逐一赋值给左手边指定的操作数;同时整个并行赋值表达式的值是右手边操作数打包成一个数组(例子中d在赋值后就引用着该数组)。
这可不是简单“借助1个临时变量”或者说“借助第三个变量”就可以解决的。

Ruby的并行赋值的语义介绍,请参考Assignments - The Ruby Programming Language [Book](这本只覆盖到Ruby 1.9,不过对本文来说够用了)

实际的实现方式有很多种,下面简单介绍一下Ruby MRI、mruby与JRuby的做法。

==============================================

Ruby MRI的情况

Ruby MRI(Matz's Ruby Interpreter),也叫CRuby,是Ruby的官方实现。
它从Ruby 1.9系列开始使用名为YARV(Yet Another Ruby VM)的虚拟机来实现Ruby代码的执行。

关于Ruby MRI的设计与实现,以前针对老的Ruby(1.7系列)有《Ruby Hacking Guide》,而现在有针对Ruby 2.x系列的新书《Ruby Under a Microscope》,这里强力推荐一下。这本书现在已经有中文版《Ruby原理剖析》,是 @张汉东 大大翻译的,虽然我还没读到中文版但我信得过汉东大大的水准~

Ruby MRI执行Ruby代码的流程是分几个阶段的:
Ruby源码
-> [ 词法分析 + 语法分析 + 简单的语义分析 ] -> Ruby AST
-> [ 字节码编译器 ] -> YARV字节码
-> [ YARV解释器 解释执行 ] -> 运算结果
因此,要理解Ruby MRI是如何工作的,我们可以借助观察某段Ruby程序对应的Ruby AST与YARV字节码来一探究竟。这样就可以知道Ruby MRI所理解的程序结构是怎样的,以及便于在Ruby MRI的源码中找到相应的实现细节。

借助一段irb session来给题主演示 a, b, c = c, a, b 的例子(以及您未来想探索此类问题的途径):
$ irb
irb(main):001:0> require 'ripper'
=> true
irb(main):002:0> require 'pp'
=> true
irb(main):003:0> s = <<SRC
irb(main):004:0" def foo(a, b, c)
irb(main):005:0"   a, b, c = c, a, b
irb(main):006:0" end
irb(main):007:0" SRC
=> "def foo(a, b, c)\n  a, b, c = c, a, b\nend\n"
irb(main):008:0> pp Ripper.sexp(s); nil
[:program,
 [[:def,
   [:@ident, "foo", [1, 4]],
   [:paren,
    [:params,
     [[:@ident, "a", [1, 8]],
      [:@ident, "b", [1, 11]],
      [:@ident, "c", [1, 14]]],
     nil,
     nil,
     nil,
     nil,
     nil,
     nil]],
   [:bodystmt,
    [[:massign,
      [[:@ident, "a", [2, 2]], [:@ident, "b", [2, 5]], [:@ident, "c", [2, 8]]],
      [:mrhs_new_from_args,
       [[:var_ref, [:@ident, "c", [2, 12]]],
        [:var_ref, [:@ident, "a", [2, 15]]]],
       [:var_ref, [:@ident, "b", [2, 18]]]]]],
    nil,
    nil,
    nil]]]]
=> nil
irb(main):009:0> 
irb(main):010:0* def foo(a, b, c)
irb(main):011:1>   a, b, c = c, a, b
irb(main):012:1> end
=> nil
irb(main):013:0> puts RubyVM::InstructionSequence.disasm(method(:foo))
== disasm: <RubyVM::InstructionSequence:foo@(irb)>======================
local table (size: 4, argc: 3 [opts: 0, rest: -1, post: 0, block: -1] s1)
[ 4] a<Arg>     [ 3] b<Arg>     [ 2] c<Arg>     
0000 trace            8                                               (  10)
0002 trace            1                                               (  11)
0004 getlocal_OP__WC__0 2
0006 getlocal_OP__WC__0 4
0008 getlocal_OP__WC__0 3
0010 newarray         3
0012 dup              
0013 expandarray      3, 0
0016 setlocal_OP__WC__0 4
0018 setlocal_OP__WC__0 3
0020 setlocal_OP__WC__0 2
0022 trace            16                                              (  12)
0024 leave                                                            (  11)
=> nil
irb(main):014:0> RUBY_VERSION
=> "2.0.0"
irb(main):015:0> 
这是在Ruby 2.0.0p481的irb上做的实验。Ripper 是Ruby标准库自带的、可以将Ruby源码parse成Ruby AST的模块,其结构与Ruby MRI自己用的AST基本一致;RubyVM::InstructionSequence 是Ruby MRI自带的、可以显示给定的代码对应的YARV字节码用的模块。
如果要看Ruby MRI内部实际使用的AST的情况,可以给ruby命令传 --dump=parsetree 或者 --dump=parsetree_with_comment 参数,例如说:
$ ruby --dump=parsetree_with_comment foo.rb
###########################################################
## Do NOT use this node dump for any purpose other than  ##
## debug and research.  Compatibility is not guaranteed. ##
###########################################################

# @ NODE_SCOPE (line: 4)
# | # new scope
# | # format: [nd_tbl]: local table, [nd_args]: arguments, [nd_body]: body
# +- nd_tbl (local table): (empty)
# +- nd_args (arguments):
# |   (null node)
# +- nd_body (body):
#     @ NODE_DEFN (line: 1)
#     | # method definition
#     | # format: def [nd_mid] [nd_defn]; end
#     | # example; def foo; bar; end
#     +- nd_mid (method name): :foo
#     +- nd_defn (method definition):
#         @ NODE_SCOPE (line: 3)
#         | # new scope
#         | # format: [nd_tbl]: local table, [nd_args]: arguments, [nd_body]: body
#         +- nd_tbl (local table): :a,:b,:c
#         +- nd_args (arguments):
#         |   @ NODE_ARGS (line: 1)
#         |   | # method parameters
#         |   | # format: def method_name(.., [nd_opt=some], *[nd_rest], [nd_pid], .., &[nd_body])
#         |   | # example: def foo(a, b, opt1=1, opt2=2, *rest, y, z, &blk); end
#         |   +- nd_ainfo->pre_args_num (count of mandatory (pre-)arguments): 3
#         |   +- nd_ainfo->pre_init (initialization of (pre-)arguments):
#         |   |   (null node)
#         |   +- nd_ainfo->post_args_num (count of mandatory post-arguments): 0
#         |   +- nd_ainfo->post_init (initialization of post-arguments):
#         |   |   (null node)
#         |   +- nd_ainfo->first_post_arg (first post argument): (null)
#         |   +- nd_ainfo->rest_arg (rest argument): (null)
#         |   +- nd_ainfo->block_arg (block argument): (null)
#         |   +- nd_ainfo->opt_args (optional arguments):
#         |   |   (null node)
#         |   +- nd_ainfo->kw_args (keyword arguments):
#         |       (null node)
#         |   +- nd_ainfo->kw_rest_arg (keyword rest argument):
#         |       (null node)
#         +- nd_body (body):
#             @ NODE_MASGN (line: 2)
#             | # multiple assignment
#             | # format: [nd_head], [nd_args] = [nd_value]
#             | # example: a, b = foo
#             +- nd_value (rhsn):
#             |   @ NODE_ARRAY (line: 2)
#             |   | # array constructor
#             |   | # format: [ [nd_head], [nd_next].. ] (length: [nd_alen])
#             |   | # example: [1, 2, 3]
#             |   +- nd_alen (length): 3
#             |   +- nd_head (element):
#             |   |   @ NODE_LVAR (line: 2)
#             |   |   | # local variable reference
#             |   |   | # format: [nd_vid](lvar)
#             |   |   | # example: x
#             |   |   +- nd_vid (local variable): :c
#             |   +- nd_next (next element):
#             |       @ NODE_ARRAY (line: 2)
#             |       | # array constructor
#             |       | # format: [ [nd_head], [nd_next].. ] (length: [nd_alen])
#             |       | # example: [1, 2, 3]
#             |       +- nd_alen (length): 140260228248800
#             |       +- nd_head (element):
#             |       |   @ NODE_LVAR (line: 2)
#             |       |   | # local variable reference
#             |       |   | # format: [nd_vid](lvar)
#             |       |   | # example: x
#             |       |   +- nd_vid (local variable): :a
#             |       +- nd_next (next element):
#             |           @ NODE_ARRAY (line: 2)
#             |           | # array constructor
#             |           | # format: [ [nd_head], [nd_next].. ] (length: [nd_alen])
#             |           | # example: [1, 2, 3]
#             |           +- nd_alen (length): 1
#             |           +- nd_head (element):
#             |           |   @ NODE_LVAR (line: 2)
#             |           |   | # local variable reference
#             |           |   | # format: [nd_vid](lvar)
#             |           |   | # example: x
#             |           |   +- nd_vid (local variable): :b
#             |           +- nd_next (next element):
#             |               (null node)
#             +- nd_head (lhsn):
#             |   @ NODE_ARRAY (line: 2)
#             |   | # array constructor
#             |   | # format: [ [nd_head], [nd_next].. ] (length: [nd_alen])
#             |   | # example: [1, 2, 3]
#             |   +- nd_alen (length): 3
#             |   +- nd_head (element):
#             |   |   @ NODE_LASGN (line: 2)
#             |   |   | # local variable assignment
#             |   |   | # format: [nd_vid](lvar) = [nd_value]
#             |   |   | # example: x = foo
#             |   |   +- nd_vid (variable): :a
#             |   |   +- nd_value (rvalue):
#             |   |       (null node)
#             |   +- nd_next (next element):
#             |       @ NODE_ARRAY (line: 2)
#             |       | # array constructor
#             |       | # format: [ [nd_head], [nd_next].. ] (length: [nd_alen])
#             |       | # example: [1, 2, 3]
#             |       +- nd_alen (length): 140260228249120
#             |       +- nd_head (element):
#             |       |   @ NODE_LASGN (line: 2)
#             |       |   | # local variable assignment
#             |       |   | # format: [nd_vid](lvar) = [nd_value]
#             |       |   | # example: x = foo
#             |       |   +- nd_vid (variable): :b
#             |       |   +- nd_value (rvalue):
#             |       |       (null node)
#             |       +- nd_next (next element):
#             |           @ NODE_ARRAY (line: 2)
#             |           | # array constructor
#             |           | # format: [ [nd_head], [nd_next].. ] (length: [nd_alen])
#             |           | # example: [1, 2, 3]
#             |           +- nd_alen (length): 1
#             |           +- nd_head (element):
#             |           |   @ NODE_LASGN (line: 2)
#             |           |   | # local variable assignment
#             |           |   | # format: [nd_vid](lvar) = [nd_value]
#             |           |   | # example: x = foo
#             |           |   +- nd_vid (variable): :c
#             |           |   +- nd_value (rvalue):
#             |           |       (null node)
#             |           +- nd_next (next element):
#             |               (null node)
#             +- nd_args (splatn):
#                 (null node)
可以对比一下这个AST与 Ripper 输出的AST的样子。

我验证过在Ruby 2.3.1p112上输出也是基本一样的(AST一样,字节码只有少量元数据的形式不同,意思还是完全一样的):
== disasm: #<ISeq:foo@(irb)>===========================================
local table (size: 4, argc: 3 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 4] a<Arg>     [ 3] b<Arg>     [ 2] c<Arg>     
0000 trace            8                                               (   1)
0002 trace            1                                               (   2)
0004 getlocal_OP__WC__0 2
0006 getlocal_OP__WC__0 4
0008 getlocal_OP__WC__0 3
0010 newarray         3
0012 dup              
0013 expandarray      3, 0
0016 setlocal_OP__WC__0 4
0018 setlocal_OP__WC__0 3
0020 setlocal_OP__WC__0 2
0022 trace            16                                              (   3)
0024 leave                                                            (   2)

可以看到,Ruby MRI在Ruby AST中是用"massign"节点来表示并行赋值表达式的(在实际的Ruby MRI AST中是“NODE_MASGN”节点类型)。
它由这条语法规则所构造:ruby/ruby/blob/v2_3_1/parse.y
stmt		: mlhs '=' mrhs_arg
                | /* ... */
                ;
顺着这条语法规则出发就可以找到构造 a, b, c = c, a, b 对应的AST的全过程,这里就不展开说了。
这个AST节点会进一步由Ruby MRI中的字节码编译器编译到YARV字节码,生成这样的字节码:
0004 getlocal_OP__WC__0 2
0006 getlocal_OP__WC__0 4
0008 getlocal_OP__WC__0 3
0010 newarray         3
0012 dup              
0013 expandarray      3, 0
0016 setlocal_OP__WC__0 4
0018 setlocal_OP__WC__0 3
0020 setlocal_OP__WC__0 2
(第一栏的数字是字节码指令在方法中的偏移量;第二栏是字节码指令的操作码(opcode);后面是可选的字节码指令的操作数(operands))

YARV虚拟机所使用的字节码指令集是一种“基于表达式栈”的指令集。它会把参数与局部变量放在一个局部变量数组里,可以用下标随机访问;而表达式计算出来的临时值则放在“表达式栈”上。
对于上面的 foo(a, b, c) 方法,YARV虚拟机的字节码使用的局部变量数组的布局为:
locals[0]:
locals[1]:
locals[2]: c
locals[3]: b
locals[4]: a
所以0004位置上的getlocal_OP__WC__0 2字节码的意思其实就是“push a”,而0016位置上的setlocal_OP__WC__0 4字节码的意思则是“store c”。

上面这段字节码,如果用Ruby语法的伪代码来表示,会是这样的:(_tmp前缀的“局部变量”表示在表达式栈上的临时值)
# getlocal + newarray
_tmp = [c, a, b]

# expandarray
_tmp1 = _tmp[0]
_tmp2 = _tmp[1]
_tmp3 = _tmp[2]

# setlocal
a = _tmp1
b = _tmp2
c = _tmp3
也就是说,在这里例子中,Ruby MRI使用了4个临时值来完成 a, b, c = c, a, b 的语义:
  1. _tmp:捕获赋值符号右手边所有操作数的数组
  2. _tmp1:将_tmp数组在表达式栈上展开后的第一个元素
  3. _tmp2:将_tmp数组在表达式栈上展开后的第二个元素
  4. _tmp3:将_tmp数组在表达式栈上展开后的第三个元素
这里之所以需要用到_tmp临时值所引用的数组,是因为Ruby方法的返回值是它最后执行的return语句的参数或者方法中的最后一个语句的值。这里这个并行赋值正好是最后一个语句,它整体的值就要作为 foo(a, b, c) 的返回值返回出去,所以代表这个值的数组就必须创建出来。

虽然irb通过Ruby内建的 eval() 来实现Ruby代码的执行,而Ruby MRI中 eval() 的实现(中间会调用rb_iseq_compile_with_option())会用YARV的默认编译选项来编译到字节码,所以这里的实验结果跟irb / eval()优不优化没有关系。

这段操作中,最有趣的部分可能就是expandarray的相关处理了。它会把指定的数组的所有元素都展开并压入表达式栈顶。它的实现在Ruby 2.3.1的YARV中在这里:vm_expandarray()

如果将实验的源码改为下面的样子来避免并行赋值的整体值被使用:
def foo(a, b, c)
  a, b, c = c, a, b
  nil
end
会看到编译生成的YARV字节码里就不再有 newarray 与 expandarray 了,而是变成:
irb(main):003:0> def foo(a, b, c); a, b, c = c, a, b; nil; end
=> nil
irb(main):004:0> puts RubyVM::InstructionSequence.disasm(method(:foo))
== disasm: <RubyVM::InstructionSequence:foo@(irb)>======================
local table (size: 4, argc: 3 [opts: 0, rest: -1, post: 0, block: -1] s1)
[ 4] a<Arg>     [ 3] b<Arg>     [ 2] c<Arg>     
0000 trace            8                                               (   3)
0002 trace            1
0004 getlocal_OP__WC__0 2
0006 getlocal_OP__WC__0 4
0008 getlocal_OP__WC__0 3
0010 setlocal_OP__WC__0 2
0012 setlocal_OP__WC__0 3
0014 setlocal_OP__WC__0 4
0016 putnil           
0017 trace            16
0019 leave            
=> nil
它的意思用Ruby语法的伪代码表示就是:
def foo(a, b, c)
  _tmp1 = c
  _tmp2 = a
  _tmp3 = b
  a = _tmp1
  b = _tmp2
  c = _tmp3
  return nil
end
也就是用3个临时值来实现这个并行赋值的例子。这对于一个“基于表达式栈”的虚拟机来说已经是比较高效的实现方式。当然咯,如果YARV能更优化一些的话,就可以连这些赋值都全部优化掉——因为后面根本没有代码使用这些赋值的效果。

前面用来做实验的Ruby MRI在构建的时候没有使用OPT_STACK_CACHING选项,所以这里的例子并没有演示出YARV的stack caching优化的效果。Ruby MRI官方发布的二进制版是没有打开该选项来构建的。
如果使用该选项的话,会看到一些带有“_SC_xx_ax”之类字样的字节码指令,那些就是stack caching相关的。这里所说的stack caching可以参考YARV的作者ko1(笹田耕一)在RubyConf 2005上做的演讲(第43-44页),也可以参考我的另一个回答:寄存器分配问题? - RednaxelaFX 的回答 - 知乎
把前面返回 nil 版的 foo() 拿来放在打开stack caching的Ruby 2.3.3上运行,其对应的YARV字节码会是这样的:
== disasm: #<ISeq:foo@foo.rb>======================================
local table (size: 4, argc: 3 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 4] a<Arg>     [ 3] b<Arg>     [ 2] c<Arg>     
0000 trace_SC_xx_xx   8                                               (   1)
0002 trace_SC_xx_xx   1                                               (   2)
0004 getlocal_OP__WC__0_SC_xx_ax 2
0006 getlocal_OP__WC__0_SC_ax_ab 4
0008 getlocal_OP__WC__0_SC_ab_ba 3
0010 setlocal_OP__WC__0_SC_ba_bx 2
0012 setlocal_OP__WC__0_SC_bx_xx 3
0014 setlocal_OP__WC__0_SC_xx_xx 4
0016 putnil_SC_xx_ax  
0017 trace_SC_ax_ax   16                                              (   4)
0019 leave_SC_ax_ax                                                   (   2)
看到那些_SC_xx_ax的指令了么?那些就是SC(stack caching)特化版指令。后面的_xx_xx表示的是这条指令执行前后的stack caching state。
YARV解释器所实现的stack caching是2层、5状态的。a与b分别是缓存表达式栈顶值的两个“寄存器”。引用ko1先生的讲解:[Yarv-devel] expandarray and opcode questions
Stack caching (SC) caches stack top. YARV's SC support 2 level
(2 stack top values) stack caching.

SC_?1_?2 means stack caching state. before instruction dispatched and
after that.

1. A, B and X
X means stack top is not cached.
A means stack top is cached by register A.
B means stack top is cached by register B.

2. XX, AX, BX, AB and BA

XX means stack top is not cached.
AX means stack top is cached by register A.
BX means stack top is cached by register B.
AB means stack top is cached by register B.
stack 2nd value is cached by regsiter A.
BA means stack top is cached by register A.
stack 2nd value is cached by regsiter B.

(不过Ruby MRI 2.3.3如果打开OPT_STACK_CACHING选项的话根本build不过,代码里有好些bug,简单修修居然还不能完全正常工作…算了,至少miniruby能build出来,上面的例子就还能跑…)

==============================================

mruby的情况

mruby是Ruby的创始人Matz带队另起炉灶做的一个轻量级Ruby实现,适于嵌入在别的程序中使用。它的实现方式,包括虚拟机/解释器与对外接口的设计都源自Lua 5.x系的思路,所以它名为RiteVM的虚拟机也跟Lua的一样是“基于虚拟寄存器”的。

来看看mruby 1.2.0对上面的返回 nil 版的 foo() 是如何实现的:
(构建好默认配置的mruby之后,执行mruby目录下的 bin/mrbc -v foo.rb 即可看到下面这段输出。mrbc是事先把Ruby源码编译到RiteVM字节码的命令)
irep 0x7fa113c0e370 nregs=8 nlocals=5 pools=0 syms=0 reps=0
file: foo.rb
    1 000 OP_ENTER	3:0:0:0:0:0:0
    2 001 OP_MOVE	R5	R3		; R3:c
    2 002 OP_MOVE	R6	R1		; R1:a
    2 003 OP_MOVE	R7	R2		; R2:b
    2 004 OP_MOVE	R1	R5		; R1:a
    2 005 OP_MOVE	R2	R6		; R2:b
    2 006 OP_MOVE	R3	R7		; R3:c
    3 007 OP_LOADNIL	R5		
    3 008 OP_RETURN	R5	return
可见这段字节码用到了若干个虚拟寄存器。其中a、b、c作为参数分别从R1、R2、R3输入。这里,R5、R6、R7则是临时变量。所以这里表达的逻辑用Ruby语法的伪代码表示就是:
def foo(a, b, c)
  r5 = c
  r6 = a
  r7 = b
  a = r5
  b = r6
  c = r7
  return nil
end
跟前面演示的Ruby MRI的情况其实基本上是一样的。

==============================================

JRuby的情况

当然,Ruby的实现并非只有Ruby MRI一家。现实中很常用的另一种Ruby实现是用Java实现的JRuby。它在并行赋值上的实现跟Ruby MRI相比又有自己的特色。
关于JRuby与Ruby MRI的对比,请跳传送门:造成Ruby和JRuby执行性能上存在差异的原因是什么? - RednaxelaFX 的回答 - 知乎

让我们用下面这个方法来做个实验:
def foo(a, b, c)
  a, b, c = c, a, b
  a
end
具体的实验内容及相关日志我放在gist上了:gist.github.com/rednaxe

随手找了个Mac OS X上的Oracle JDK 7u45,在它的HotSpot VM上运行JRuby 9.1.6.0,当JRuby的JIT把这段Ruby代码编译到Java字节码之后, foo(a, b, c) 的方法体被编译为等价于这样的形式:
def foo(a, b, c)
  tmp1 = c
  tmp2 = a
  tmp3 = b
  a = tmp1
  b = tmp2
  c = tmp3
  return a
end
跟前面的Ruby MRI与mruby都很相似对不对?
然后再交给HotSpot VM的JIT完成编译后,这个 foo(a, b, c) 方法就被优化到了等价于这样的形式:
def foo(a, b, c)
  c
end
用Oracle JDK 8u101做了同样的实验,结果也是基本一样的。

它JIT编译出来的机器码是这样的:
  # {method} {0x000000010dd1f108} '__file__' '(Lrubyjit/Object$$foo_4bf0e5f1cc4a54c5f4e2f8727e91176c740fdd231118140819;Lorg/jruby/runtime/ThreadContext;Lorg/jruby/runtime/builtin/IRubyObject;Lorg/jruby/runtime/builtin/IRubyObject;Lorg/jruby/runtime/builtin/IRubyObject;Lorg/jruby/runtime/builtin/IRubyObject;Lorg/jruby/runtime/Block;)Lorg/jruby/runtime/builtin/IRubyObject;' in 'rubyjit/Object$$foo_4bf0e5f1cc4a54c5f4e2f8727e91176c740fdd231118140819'
  # parm0:    rsi:rsi   = 'rubyjit/Object$$foo_4bf0e5f1cc4a54c5f4e2f8727e91176c740fdd231118140819'
  # parm1:    rdx:rdx   = 'org/jruby/runtime/ThreadContext'
  # parm2:    rcx:rcx   = 'org/jruby/runtime/builtin/IRubyObject'
  # parm3:    r8:r8     = 'org/jruby/runtime/builtin/IRubyObject'
  # parm4:    r9:r9     = 'org/jruby/runtime/builtin/IRubyObject'
  # parm5:    rdi:rdi   = 'org/jruby/runtime/builtin/IRubyObject'
  # parm6:    [sp+0x20]   = 'org/jruby/runtime/Block'  (sp of caller)
  0x0000000105c45580: sub    $0x18,%rsp
  0x0000000105c45587: mov    %rbp,0x10(%rsp)    ;*synchronization entry
                                                ; - rubyjit.Object$$foo_4bf0e5f1cc4a54c5f4e2f8727e91176c740fdd231118140819::__file__@-1

  0x0000000105c4558c: mov    %rdi,%rax
  0x0000000105c4558f: add    $0x10,%rsp
  0x0000000105c45593: pop    %rbp
  0x0000000105c45594: test   %eax,-0x1bc159a(%rip)        # 0x0000000104084000
                                                ;   {poll_return}
  0x0000000105c4559a: retq   
其中rcx、r8、r9、rdi分别代表Ruby层面上 foo() 的 self、a、b、c 参数。可以看到JIT编译出来的机器码只做了一件事情,就是把 rdi 赋值到 rax,也就是返回参数c的值。

所谓“局部变量赋值”,无论是普通赋值还是并行赋值,其实都只是要把一个值绑定给一个名字而已。经过充分编译优化后,这些局部变量赋值常常可以被完全优化掉,只留下真正有用的值。
这个例子里,在 foo() 中并没有 eval() 或者其它能获取 foo() 的bindings的代码,所以对局部变量的赋值对外界来说是“不可观测”的,编译器就可以对其做充分的优化。

下面是一些链接笔记,备份用。

.jrubyrc
# Return true from multiple assignment instead of a new array.
# Options: [true, false], Default: false.

#compile.fastMasgn=false
RubyInstanceConfig (JRuby Core 1.7.20 API)