mrubyのJITの概要


はじめに

 かれこれ5年ほどmruby にTracing JITを加えた "mrubyのJIT"を作っています。あまり話題にはならないのですが、その理由としてmrubyのJIT全体を説明した文章が無いからじゃないかと気付きました。そういうわけで簡単な紹介と詳細な情報のリンクを載せた文章を書くことにしました。

mrubyのJITの概要

mrubyのJITは実行時に頻繁に実行するmrubyのバイトコードの命令を機械語命令に変換します。x86(32bit)とx86-64(64bit)で動きます。はじめは32bit用で開発し、あとから64bitに対応しました。64bitに対応することによる技術的な詳細はこちらをご覧ください。

Xbyak

mrubyのJITのコード生成にはherumiさんが製作されたXbyakを使わせてもらっています。いままでXbyakが原因となるトラブルやバグは一度も経験がありません。素晴らしいことです。

実行速度

JITコンパイラといえば誰しも速度を期待することでしょう。実行速度はこんな感じです。
(CPU i5, Windows7上のCygwinで計測)

ベンチマーク mruby mrubyのJIT
ao-bench   30.46   3.15
セクシー素数 19.48      1.14
マンデルブロート 13.47     1.63
ペントミノ 249.8 43.16
ニューラルネット 19.17 2.59

まあ、VMをぶん回すようなプログラムだと、大体5~10倍くらいの速度が出るようです。文字列処理やIOなどCで書かれたライブラリを多用すると速度の差は縮まるでしょう。

内部構造

mrubyのJITはTracing JITです。Tracing JITそのものはググってください。mrubyのVMのバイトコード命令をフェッチして命令毎の処理に分岐するところで制御を乗っ取って実行回数をカウントします。実行回数がある閾値になったら機械語を生成し、次にこの命令を実行したら生成した機械語を呼び出します。
概要をつかむのは名古屋Ruby会議03で発表させてもらったスライドがいいと思います
https://www.slideshare.net/miura1729/mrubyjit
内部構造の詳細はこちらを見てください
あなとみー おぶ mrubyのJIT (その1)
このあと、連続していてその13まであります。

最適化

とてもささやかですが、機械語を生成するときに最適化をおこなっています。詳細はこちら
mrubyのJITの最適化事典
また、mrubyのJITだけではなくmrubyでも有効な最適化を入れています。非常にリスキーな最適化(でメモリを湯水のように使う)なので本家mrubyにはプリクは出していません。
mrubyの可変引数最適化

変態仕様対応

Rubyは言語仕様が変態なのでJITコンパイラも面倒なことをする必要があります。どういうふうに面倒くさいのかはこちらのスライドをご覧ください(うーん発表したかったな)。
https://www.slideshare.net/miura1729/mruby-jit-63680153
Rubyはメソッドが再定義出来るのですが、これに対応するための方法の詳細はこちらになります。
mrubyのJITにおけるメソッド再定義の対処方法

関連プロジェクト

mrubyのJITに関連して、いくつかのmrbgemを作りmrubyのJITに同梱しています。

PArray

ベクトルライブラリです。今の所4要素のみサポートしています。このライブラリの特徴はmrubyのJITと一緒に使うとSSEのベクトル命令が生成されることです。でも、目立った効果は得られませんでした。残念。詳しくは
mrubyのJITのPArrayモジュール

MMM

ここから先このオブジェクト使わないから再利用してって処理系に教えることで、GCを黙らせることが出来るライブラリです。使いどころが決まれば効果があります。でも通常メモリアロケーションのボトルネックを突き止めるのは難しいですよね。何かいいツールがあれば別ですが。詳しくは
mruby-mmmの紹介

mruby-inline

メソッドのインライン化を行う夢のライブラリです。インライン化するところ手動で指定する必要があるし、大抵落ちるので現状この夢は悪夢です。ちゃんと動けばちゃんと速くなります。詳しくは
mruby-inlineドキュメント

欠点やこれからの話

マルチスレッドに対応していない

H2Oという素晴らしいHTTPサーバーがあるんですよ。H2Oってmrubyが組み込まれていてmrubyで色々面白いことが出来るわけですね。このmrubyをmrubyのJITに置き換えたらどうだろう?って普通思いますよね。で、やってみたんですよ。H2Oは64bit環境じゃないと動かないから64bit対応も行って。動いたら何倍とは言わないけど数割速くなったわけです。じゃー、宣伝をしてみんなに使ってもらえばいいじゃんって思うよね。私はそう思わないけど。
なぜか?H2Oはマルチスレッドでmrubyを複数のmrb_stateを用意して並列に動かしているんです。しかし、mrubyのJITはJITコンパイルで使うデータ構造がmrb_stateで分けていないのでマルチスレッドになったとたん(つまり複数から同時にアクセスされると)、落ちるんです。
マルチスレッド化なんて、コンパイルに関するデータ構造にロックを掛けたり、コンパイラのところをクリティカルリージョンにするとかいくらでもやり方があるじゃん、なんなら特定のスレッドだけコンパイルして他のスレッドはその生成コードを使うだけってことも考えられるでしょう。
でも簡単ではないんです。mrubyのJITはコードパッチを多用することを思い出しましょう。つまりネイティブコードそのものが排他制御の対象になってしまうのです。
そんなわけで、なかなか大変ですがなんとかマルチスレッドに対応したいなと思っています。

型推論

どこにオーバーヘッドがあるか日々プロファイルを採ってにらめっこしているのですが、型のガードが結構重いことが分かっています。でも、動的型付けでガードを減らすのはどうしても限界があります。それ以上はやはり型推論が欲しくなります。

もっと賢い最適化

現在のmrubyのJITは最適化をおこなっていますけど所詮は覗き穴です。もっと賢い、例えば変数をレジスタに乗っけるとかそういうのはできません。そういうのを行うのには現在のmrubyのバイトコードは力不足です。やはりデータや制御の依存関係が簡単にわかるSSAのような中間表現が望まれます。現在のJITコンパイラはコンパイル速度が速いというメリットがあるので、残してさらに速度が必要な時にSSAを生成しコンパイルするという流れが妥当かなと思います。

コード領域のGC