MJIT で dlopen 使わずに ELF オブジェクトを直接ロードする話

MJIT というのが Ruby に入ったのは聞いていて、すごいことするな、と思ってたんですが、実際に Ruby Kaigi で話を聞けて少し遊んでみたくなったのでした。そういえば https://turingcomplete.fm/5 の時に「MJITについてどう思うか聞いておいて下さい」とかリクエストしておいたのに聞いてくれなかったのであった。

https://k0kubun.hatenablog.com/entry/ruby26-jit

すごいことするな、と思ったのはその手法で、 C 生成して dlopen という、よく雑談とかで言う話ではあるけど、実際広く使われる用途で使われたのは見たことが無かった(ICFPCとかでは見たことがある)ので、すごいなと。

一方で、 dlopen たくさんすると、いくつかの意味でオーバヘッドがかかると思われるため、あとでマイクロベンチに出ないところで大変だったりするだろうなぁ……特にメモリのローカリティ的な、とか思いつつ聞いていると、やはりメモリのキャッシュヒット率が減りはじめると悪い影響が……という話になりました。一つ一つの C レベル関数は恐らく小さいのではないかな、と思っていたので、コードサイズにして例えば 100-1000 バイトくらいの関数のために、 dlopen すると少なくとも必要であろう r-x の .text ページと GOT のための r-- (or rw-) のせいで 8192 バイトを使うことになるのではないかなぁ……と思っていたのでした。

Ruby Kaigi 中に質問したのですが、「一つの関数ごとに最低 8 or 12kB 使うかな……」と思っていたところに「2MB/method!」みたいなスライドを見て、なんじゃそれと思ったからでした。あとで スライド をよく眺めると、 .text と .rodata の間のいじれないやつもカウントしてただけと気付きました。 2MB の仮想アドレスを取っちゃうのもどうなんだって話はあるかもしれないですが、それより3つのページに分割されるのが本質的な問題だと思います。かつ、 MJIT の吐いてるコードを見る限り .data と .rodata は使わなさげなので、実際に触るページは .text 用のやつと .got のやつで2枚だけでないかと思います。

さて、この問題についての正しい解決策は、適切な単位で複数のメソッドをまとめてコンパイルする、というものじゃないかと思います。でかつそれは提案されていた手法なので良いと思います。とはいえ .got のために .so 一つごとに無駄に 4kB 使うというのもなぁ……などとも思います。で、 .got と .text をまとめて同じページに突っ込んでくれそうなリンカオプション無いか……と探したり(なかった)、そもそも RELRO て .got を RX にすることできるの……とローダ眺めたり(できなかった)しました。

なーんていうことを考えてから、とりあえずムダをはぶくだけなら .so をローダに読ませるのではなくて、 .o を自力で適当にロードすればいいよね、ということで、そういうものをでっちあげました。

https://github.com/shinh/ruby/tree/objfcn

まあ……使いやすいものではないと思います。本質的に MJIT のような .so を読む以外の手法で書かれた JIT エンジンにある問題ですが、やはりデバッグは大変ですし、自分でちゃんと空いたメモリを解放するとかも面倒ですし(現状1GB固定アロケートするモードと遅い方法)、まあそんなこんな……

ただ、 C コード吐いてコンパイルしてロードする、って方針が変わらないのであれば、ロードに関しては最速な方法だと思うので、もっと適切な方法であろう、複数のコードをまとめてコンパイルする、という方法で理論限界に近付いてるかどうかは、私のコードと比較すれば評価できるのではないかと思っています。

あと最初にがーと書いたコードは2つクラッシュするバグがあって、それぞれなるほどなあと思ったので書いておきます。

一つ目は mmap を前もってする際に PLT/GOT のために生成するコードのサイズを計算に入れてない、というものでした。これは tinycc の時もどうしたもんかと思った記憶がありました……

二つ目はセクションヘッダに指定されてるアラインメントを適切に取っていない、というものでした。具体的には SSE 命令で data 領域を読んだりするコードの SSE アラインメントが壊れてる、という話でした。なんか低レイヤなとこいじってると SSE アラインメントは必ずバグりますね。スタックがアライン取れてない状態で printf 呼んで死んだりとか……

とりとめもないですが、こんな方法を MRI で採用すべきとは思ってないのですが、ただこの手法の JIT エンジンの持つ、いくつかあるオーバーヘッド源の一つであろう、メモリがページ境界で分断されてしまってローカリティが良くない、というやつがどのくらい悪さをしてるかの評価程度には使えるのではないかと思っています。

なにかあれば下記メールアドレスへ。
shinichiro.hamaji _at_ gmail.com
shinichiro.h