最近のプログラミング言語処理系を見ていると、バックエンドをLLVMに任せているものがちょいちょいあります。Rust, Swift, Juliaなどは一例に過ぎません。
私もプログラミング言語処理系の開発者として、LLVMを使うコンパイラーを触ったりしています。GHCのLLVMバックエンドを触ったり、SML#をAArch64に対応させたり、HaskellからLLVMを呼び出してJITコンパイルしたり、です。
LLVMは強力で便利なのですが、依存することのデメリットも感じるようになってきました。
LLVMに依存することのデメリットその1:追従の手間
LLVMは今も活発に開発が続いているソフトウェアです。半年に一回くらいのペースで新しいメジャーバージョンがリリースされているようです。
LLVMのバージョンが変わると微妙に非互換があったりするので、利用者は特定のバージョンのLLVMを念頭に置いて言語処理系を作成することになります。
では、新しいLLVMが出たときはどうするべきでしょうか?追従することを選択すると、そのための作業が必要になります。開発が活発ではない言語処理系を見ていると、追従が追いついていないケースが見受けられます。
一方で、追従しないことを選択すると、そのバージョンのLLVMが入手可能な間は良いのですが、いずれ古いLLVMはパッケージマネージャーから削除されるので、最新の環境では利用不可になります。例えば、Homebrewで llvm@12
をインストールしようとすると次の警告が出ます:
Warning: llvm@12 has been deprecated! It will be disabled on 2025-07-01.
Ubuntuでも、Ubuntu 24.04 LTSの初期設定のaptで入るLLVMは14以降のようです。
私が見ている中では、GHCは割と追従しています。GHC 9.12はLLVM 13以上19以下に対応しています。ただ、「GHCへの私の貢献2024」にも書いたように、LLVMの変化に対応できていなかった(のを私が見つけた)ケースがちょいちょいあります。
SML#は開発が止まっている期間もありましたが、最近出た4.1.0でLLVM 7.1以降18.1以下の対応になったようです。
LLVMに依存することのデメリットその2:独自の呼び出し規約
関数型言語とかだと末尾呼び出し最適化の都合などで独自の呼び出し規約を採用したい場合がありますが、独自の呼び出し規約を実装するにはLLVMに手を加える必要があります。
GHCは独自の呼び出し規約をLLVMに取り込んでもらえたので良いのですが、最近LLVMへの移行を進めているSML/NJは私が知る限りではまだ呼び出し規約をLLVMに取り込んでもらえていないので、素人目に見て大変そうです(自分達用のフォークを維持する必要がある)。
パフォーマンスを追求しないのであれば、独自の呼び出し規約を設定するのではなく、C呼び出し規約の上でトランポリン等の技法を使って末尾呼び出し最適化等の必要な機能を実装するという手もあります。
LLVMを使うメリット:SIMD
それでもLLVMを使うことのメリットは大きいわけです。レジスター割り付けやアセンブリコードの出力を任せられるとコンパイラー作成者は楽ができます。
「HaskellでEDSLを作る:LLVM編 〜JITコンパイル〜」では、LLVMに自動ベクトル化をさせました。自動ベクトル化を自前でやるのは結構大変なのではないでしょうか。ターゲットとなるSIMD命令セットはx86だけでもSSE/AVX/AVX-512とありますし、ArmにもNEON/SVEなどがあります。
というか、手動でSIMDを使う場合であっても、LLVMの抽象化はありがたいです。
私が最近GHCのNCG(LLVMに依存しないバックエンド)をx86のSIMDに対応させる作業をやっているのは「GHCへの私の貢献2024」にも書いた通りですが、x86のSSEはかなりアドホックな体系をしています。SSE4.1とかAVXになると多少マシになりますが、ISAのベースラインがSSE2とかだったりするので、SSE2を切り捨てるのでなければ「SSE2向けのコードを出力できるモード」を実装する羽目になって結局苦労します。
例えば、ベクトルの特定の位置の要素を別の値で置き換える処理を書くとき、SSE2だとPINSR系命令は16ビット要素(PINSRW)しか使えないため、int8x16, int32x4, int64x2の処理は個別に実装する必要があります。
特定の値で埋まったベクトルを作る(ブロードキャスト)のも、x86だとV(P)BROADCAST系の命令が使えるのはAVX/AVX2の世代からで、SSEでやるにはマニュアルと睨めっこして複数の命令を組み合わせる必要があります。
これが、LLVMを使えば体系的なベクトル型と命令がある(「特定の位置の要素を置き換える」ならinsertelement)ので、大幅に楽ができるというわけです。
私が作っているLunarMLはまだネイティブコード出力を実装する段階ではありませんが、ネイティブコード出力をしたくなったら「LLVMを使うかどうか」を考える必要があります。やるとしたら「Cコード経由」「LLVM経由」を選択できるようにする感じでしょうか。