msumimz's diary

RubyにJITコンパイラを実装する個人プロジェクトの情報発信ブログです。

LLVMのJITサポートの現状

前回の続きで、せっかくなので、LLVMJITサポートの現状をまとめておきます。

現行のLLVM 3.4のJITコンパイラサポートは、古くから存在するシンプルな仕組み(old JITと呼ばれます)と、MCJITと呼ばれる新しい仕組みの2種類が併存しています。今後は、old JITのサポートはやめ、MCJITに一本化する方針です。

old JITは、関数を都度コンパイルするだけのシンプルな仕組みです。未コンパイル関数の呼び出しがある場合は、スタブを生成して、呼び出し時に自動的に遅延コンパイルするようになっています。

一方MCJITは、モジュール単位でJITコンパイルを行い、コンパイル結果として完全なELFオブジェクトを生成するという重量級のアーキテクチャです。old JITにあったような遅延コンパイルの機能はないため、関数定義はモジュール内で完結するか、外部リンクにしなければなりません。また、一度コンパイルしたモジュールを再コンパイルすることもできません。そのため、実用上は、メソッドコンパイルするたびに、そのためのモジュールを作成することになるという、動的言語JITコンパイラのインフラとして、いささか疑問を抱かせる設計になっています。

メリットとしては、コンパイル結果のキャッシュが可能になるほか、デバッガサポートが改善するとされています。これはELFオブジェクトを生成する成果ですが、JITコンパイラに必須の機能とはいいがたいものです。

MCJITの設計については公式のドキュメントがあります。

MCJIT Design and Implementation
http://llvm.org/docs/MCJITDesignAndImplementation.html

また、LLVMの公式ブログの一連の投稿で、old JITとのパフォーマンス比較がされています。

Using MCJIT with the Kaleidoscope Tutorial
http://blog.llvm.org/2013/07/using-mcjit-with-kaleidoscope-tutorial.html
Kaleidoscope Performance with MCJIT
http://blog.llvm.org/2013/07/kaleidoscope-performance-with-mcjit.html
Object Caching with the Kaleidoscope Example Program
http://blog.llvm.org/2013/08/object-caching-with-kaleidoscope.html

投稿記事では、コンパイル時間の増大はコンパイル結果のキャッシュにより補填されると主張しています。しかし、実用的なJITコンパイラは実行時プロファイリングに基づいてspecializeされたメソッドコンパイルするでしょうから、キャッシュが有効に機能するか疑わしいように思います。キャッシュを行わない場合、コンパイルの所要時間はold JITより増大します。また、コメント欄で指摘されているように、メモリ消費量も増加しているはずですが、比較がありません。

実際にコードを書く立場から見ても、メソッドコンパイルする都度モジュールを生成しなければならないため、単純に手間がかかります。old JITのように、事前にヘルパ関数を定義しておくこともできません(モジュールをまたいだ外部リンクなら可能ですが、関数の宣言自体は毎回挿入しなければなりませんし、性能上も好ましくありません)。

じゃあold JITを使い続ければよいじゃないかと思われるかもしれませんが、今後、新機能はMCJITのみに実装されることになっており、それには、stack map/patch pointと呼ばれる、onstack replacementの実装に必要な機能も含まれています。old JITの開発が停止することも考えると、MCJITへの移行は避けられなさそうです。

とはいえ、移行したところでよほど効果的なキャッシュを導入しない限り性能が落ちるのがわかっていますし、前回の記事にあったように、今後アーキテクチャの変更も入りそうな情勢で、あまり気がすすみません。しばらくは悩ましい時間が続きそうです。

まあ、現在の進捗状況では、old JITで困るような段階に達してもいないわけで、悩んでる時間にコード書けよって話ではあるのでした。