llilcについて

Microsoftがcoreclrまわりの新しいネタを投入してきましたね。LLVMを.NET Coreで使うものだそうです。

github.com

"an LLVM based compiler for .NET Core." 何だかよくわからないですね。

llilcは、.NETで言えばランタイムの一部分の一種であり、99%の.NET開発者には意識する必要のないプロジェクトです。

.NETランタイムには、MSILを解釈して、実行マシン上のCPUネイティブコードに変換して実行する機能が求められます。いわゆるJITコンパイラーというやつです。LLVM based "compiler"というのは、このJITコンパイラーのことです(RoslynみたいなC#コンパイラーとは、取り扱っている問題が違います)。

オープンソースで公開されているcoreclrには、現在、RyuJITというJITコンパイラーがあります。このJITコンパイラーは差し替え可能なcoreclrの部品なので す。llilcは、その代替候補のひとつとなります。Javascriptの世界では、よく新しいJITが導入されて(v8, spidermonkey, chakra, ftljit…)、そのパフォーマンスが議論されますが、MSIL JITの世界でも同じことが起こっているということですね。

これらのJITの実装にはさまざまな特徴があり、Javascriptの世界だと、大概は「新しいほうがパフォーマンスが良い」みたいな文脈で語られますが、実際には一長一短の世界でもあります。RyuJITはWindowsで動作する.NETのMSILに特化したJITとして開発されてきたものであり、MSILを最適に実行できるJITとして在るのだと思いますが、LLVMほど多様な環境を前提としてはおらず、移植性(あるいは「可能性」)は、LLVMほど高くはないでしょう。

JITで活用されるLLVMフロントエンド、とは何なのか?

llilcはLLVMフロントエンドのひとつとして実装されています。この意味が分かる人は、ここからしばらく読み飛ばしてもらっても大丈夫ですが、LLVMについてよく分かっている人は多くないと思うので、この辺で少し解説しておこうと思います。

LLVMは "compiler infrastructure" なのですが、LLVMが主にターゲットにしている「コンパイラー言語」は、ネイティブコードを直接生成するC++のような言語です。われわれは、コンパイラーを作成する時、まずソースコード構文解析してシンタックス ツリー(AST)を構築し(文法エラーなどをレポートし)、そのASTを解釈してコードのセマンティック モデルを構築し(おかしなコードの定義や参照をレポートし)、そこから仮想的な実行命令(IR*1)の集合を生成します。この実行命令の集合は、プラットフォームやアーキテクチャに特化していないもの(そのようにあることを意図したもの)であり、実行するためにはこれを各プラットフォーム/アーキテクチャのネイティブコードに置き換えるものが必要です。

LLVMでは、この仮想的な実行命令の集合を構築するまでの部分をLLVMフロントエンド、仮想的な実行命令の集合からCPU固有のネイティブコードを生成する部分をLLVMバックエンドと呼びます。これらが切り離されていることで、LLVMは、多様なコンパイラー言語(のLLVMフロントエンド)から、さまざまなプラットフォーム用の実行コードを(LLVMバックエンドで)生成できるような "compiler infrastructure" であるわけです。例を挙げるなら、AppleObjective-Cコンパイラはフロントエンド、emscriptenのようなプロジェクトは(特殊な)バックエンドですね*2

LLVMフロントエンドが.NET Coreをサポートするというのは、まだよくわかりませんね。.NETはMSILを実行するためのものです。LLVMコンパイラーです。llilcはC#ソースコードを解析するLLVMフロントエンドなんでしょうか?

そういうことではありません。llilcにとっての「ソース」は、C#ではなく、MSILなのです。そして、llilcはコンパイラツールとして使われるわけではないのです*3C#からMSILを生成するのは、これまで通りC#コンパイラー(csc, mcs, roslyn)の仕事です。llilcは、CoreCLRにおけるJITコンパイラーとして使われます。言い換えれば、LLVMはllilcのJITコンパイラインフラストラクチャー」となるわけですね。

ちなみに(ここに来てようやく)monoの話をしますと、monoにも、開発初期からずっと存在しているMSILのみをターゲットとするコード生成エンジンと、LLVM IRを生成するmono-llvmエンジンのふたつが存在します。これらは、JITの側面では、概ねRyuJITとこのllilcの関係に近いと言えるでしょう。mono-llvmはmonoのエンジンに比べてサイズが大きく、Xamarin.Androidのような環境では、まだデフォルトとしては使えません。

(ちなみにmonoの実装の中では、"LLVM Backend"という表現が使われていますが、monoの実行エンジンのbackendという意味であって、mono-llvmLLVMの世界におけるLLVM backendとして実装されているわけではありません。あーややこし。)

AOT実装について

JITのコード生成部分が差し替え可能な環境でしばしば挙げられる目標として、ネイティブコードに直接変換した上で実行できるような事前コンパイル(AOT; ahead of time compilation)が挙げられます。llilcは意欲的で、このAOTサポートも目的の一つとしています(まだ出来ていません)。動的コード生成がAppleによって独占されているiOSのような環境で使おうと思うと、AOT対応が必須になってきます。

llilcが特に興味深いのは、コード生成部分だけが異なるmono-llvmとは異なり、llilcのAOTはcoreclrを実行時に組み込む前提としていないところです。llilcのアーキテクチャ ドキュメントには、JITとAOTのモデルが図示されているのですが、

llilc/llilc-arch.md at master · dotnet/llilc · GitHub

AOTの方にはCoreCLRのブロックが見当たりません。アーキテクチャのドキュメントでは、詳しいことはこれから書く、みたいに書かれているのですが、どうも実行エンジンそのものがcoreclrから独立している感じですね。そのせいか、llilcで実行する場合にはメモリ管理をどうするか(LLVMに含まれるGC実装を使う)、とか、例外のpropagationをどうするか、とかいった話が、割と詳しく書かれているようです。

(これらはAOT固有の話というわけではなく、coreclrに実行エンジンを組み合わせる場合にはここから実装しなければならないということかもしれません。monoのLLVMサポートでは、独自にメモリ管理まわりを繋ぎ込んでいることは ない よう です。)

この辺は今後出てくるドキュメントとコード次第ですね。

ちなみに、RyuJITに対応するAOTは今のところ存在しないと思いますが、.NETの世界では、.NET Nativeというものがあって(と言っても正式版はまだでしたか)、.NETのコードをネイティブアプリケーションにして配布できるようになっているのですが、あれもAOT技術のひとつなので、coreclrのメインストリームにおけるAOTの実態は既にあるか、準備出来つつあるということなのではないかと思います。

というわけで、llilcがどんな位置付けにあるのかをざっと解説してみました。LLVMを使うことで、RyuJITでは手が回らないようなプラットフォーム / アーキテクチャでも.NET Coreのコードが動くとなると、なかなか魅力的である気がします。

まあ、マネージドコードを書くだけの開発者にとっては、これで大きく可能性が広がったというわけでは特に無くて、主に、ランタイムを.NETが動かないプラットフォームに組み込んで使いたい人なんかが、こういったプロジェクトで恩恵をこうむるんじゃないかとは思います。

*1:intermediate representation https://idea.popcount.org/2013-07-24-ir-is-better-than-assembly/ が参考になるでしょう

*2:「プラットフォームの実行コードとして」Javascriptを生成するわけですね

*3:最終的には、AOTコンパイラーとしての部分とroslynを繋ぎ合わせて、ソースから実行ファイルを生成するようなツールが作られるかもしれませんが、ここで書いていることはそういう話ではありません