ローファイ日記

出てくるコード片、ぼくが書いたものは断りがない場合 MIT License としています http://udzura.mit-license.org/

YJITがRubyの命令を機械語にコンパイルした結果を眺める

Rubyを、以下のオプションでビルドするとYJITのデバッグ機能が使える(rbenvを利用する例)。

$ export CONFIGURE_OPTS="--enable-yjit=dev"
$ rbenv install 3.4.2

その中にはISeqが機械語コンパイルされた結果を確認する RubyVM::YJIT.disasm(isec) というメソッドがある。いくつかRubyのコードでの結果を試してみた。

試した環境など

  • OS: x86_64 Linux、ただし Macbook M3 Pro の環境で、 qemu を使ってエミュレートした環境
  • ruby -v --yjit: ruby 3.4.2 (2025-02-15 revision d2930f8e7a) +YJIT dev +PRISM [x86_64-linux]

検証用のコード

なんちゃない、足し算の最適化。西海岸のいつもの味です。

def target
  10 + 20
end

10000.times { target }

isec = RubyVM::InstructionSequence.of method(:target)
puts RubyVM::YJIT.disasm(isec)

ちなみに一部のメソッドは特別な最適化がされる。今回は Integer#+ をターゲットにする。どれがされるのかは...。

NEWSを直接当たるのが一番わかりやすいらしい。

結果

$ ruby --yjit /tmp/jit.rb 
== disasm: #<ISeq:target@/tmp/jit.rb:1 (1,0)-(3,3)>
0000 putobject                              10                        (   2)[LiCa]
0002 putobject                              20
0004 opt_plus                               <calldata!mid:+, argc:1, ARGS_SIMPLE>[CcCr]
0006 leave                                                            (   3)[Re]

NUM BLOCK VERSIONS: 1
TOTAL INLINE CODE SIZE: 58 bytes
== BLOCK 1/1, ISEQ RANGE [0,7), 58 bytes ======================
  0x7b1fdcc110c6: mov esi, 0x15
  0x7b1fdcc110cb: mov edi, 0x29
  0x7b1fdcc110d0: mov rax, rsi
  0x7b1fdcc110d3: sub rax, 1
  0x7b1fdcc110d7: add rax, rdi
  0x7b1fdcc110da: jo 0x7b1fdcc1310a
  0x7b1fdcc110e0: mov rsi, rax
  0x7b1fdcc110e3: mov eax, dword ptr [r12 + 0x20]
  0x7b1fdcc110e8: test eax, eax
  0x7b1fdcc110ea: jne 0x7b1fdcc13132
  0x7b1fdcc110f0: add r13, 0x38
  0x7b1fdcc110f4: mov qword ptr [r12 + 0x10], r13
  0x7b1fdcc110f9: mov rax, rsi
  0x7b1fdcc110fc: jmp qword ptr [r13 - 8]

0x7b1fdcc110c6 ~ 0x7b1fdcc110d7 までがコンパイルされたコードだねとわかる。ではこれは何?

この区間を翻訳すると以下のようになる。

  0x7b1fdcc110c6: mov esi, 0x15 ;; a = 0x15 = 21
  0x7b1fdcc110cb: mov edi, 0x29 ;; b = 0x29 = 41
  0x7b1fdcc110d0: mov rax, rsi  ;; ax = a
  0x7b1fdcc110d3: sub rax, 1    ;; ax -= 1
  0x7b1fdcc110d7: add rax, rdi  ;; ax + b = 61

10 + 20 じゃなくね? ってまず思った。

だが、よく考えたら

21, 41, 61 とは

irb(main):001> 10.object_id
=> 21
irb(main):002> 20.object_id
=> 41
irb(main):003> 30.object_id
=> 61

一般に、 Ruby のInteger N のobject_idは 2 * N + 1 で求められる*1。すなわち、上記のアセンブラは、object_idを直接演算していたということになる。

どうしてそうしてるのかは不明(もしかしたら誰かの発表を見逃している)だけど、object_idを直接扱うとVMに優しいのかもしれない。

という小ネタ。

*1:あくまで今の仕様?