LLVMとclangを試してみる
最近噂になってるLLVMを試してみました。
LLVM(Low Level Virtual Machine)は任意の言語を用いて多段階の最適化を試みるコンパイラ基盤だそうで、
従来の処理系ではコンパイルレベルのローカルな(主にファイルやモジュール単位の)最適化が限界なのに対して、
言語に依存しないリンカーレベルのグローバルな(最終的なアセンブリでの)最適化が行えます。
また、中間言語や仮想機械、JITコンパイラなどを用いる意味ではJavaや.NETの構想に近いですが、
実行時最適化だけでなくネイティブコード(というかこっちが現状でデフォルト)も吐ける他、
GCCとのABI(バイナリ)互換も有効らしいので、簡単に過去の遺産を有効利用できます。
GCCに頼らないBSDライセンスのコンパイラ群であるclangが、FreeBSDやLinuxをビルドできるようになったということで、
最新のGPL呪縛を逃れる為に*BSD勢は乗り換え開始してるし、
開発には多くのAppleエンジニアが関わってるみたいで、実際にAppleの主要な製品の幾つかに利用されている。
そろそろ本格的に利用できる時期なんじゃなかろうか、という意見が多いようですので、ちょっと流行に乗ってみます。
Gentooの場合は、llvm-2.8-r1が十分テストされてstable状態なのでemergeで一発導入可能。
clangや、GCCベースのllvm-gccはテスト中ですが~ARCHキーワードを受け入れれば最新版が導入可能です。
さっそくclangのリンク時最適化を試してみましょう。
ベンチマークとかはノウハウ全然分からないので他の方の記事に任せますが、
clangやllvm-gccの吐くコードの実行速度は現状では処理によりけりで、標準のgccより速かったり遅かったり、
時にはMSVCより速いコードを吐いたりする時もあるので今後に期待です。ICCには負けるけど。
以下の二つのファイルで実行ファイルを作ってみます。
[test1.c]
#include <stdio.h> extern void (int*, int*); int main(void) { int x = 1, y = 2; swap(&x, &y); printf("x = %d, y = %d\n", x, y); }
[test2.c]
void swap(int *x, int *y) { int tmp = *x; *x = *y; *y = tmp; }
まずは普通に標準GCCで最適化オプション付きで、順を追いながらコンパイルしていきます。
$ gcc -c -O2 test1.c (プリプロセス、コンパイル、アセンブル。test1.oができる。) $ gcc -c -O2 test2.c (プリプロセス、コンパイル、アセンブル。test2.oができる。) $ nm test1.o test2.o test1.o: U __printf_chk 0000000000000000 T main U swap test2.o: 0000000000000000 T swap (シンボルのリストアップ。printfが内部で最適化済みの__printf_chkに差し替えられてる!) $ gcc test1.o test2.o (リンク、ロード。a.outができる。) $ nm a.out | grep -v " _" 0000000000601010 W data_start 0000000000400570 T main 00000000004005c0 T swap (シンボルのリストアップ。内部シンボルはとりあえず興味無いので表示しない。) $ ./a.out x = 2, y = 1 (実行可能。)
結果として、目に見える形ではtest1.c内の標準関数printf呼び出しだけが最適化されたのが分かりますが、
流石に最適化オプションを付けたところでswap関数が展開されたりはしません。
-O3を付けたところでこれは同じで、ファイル単位の最適化をした後にリンクをする従来の方式では、
これが限界だということが分かります。
さきほどのgccの処理を、clangに置き換えることも可能ですが、この場合gccと同じくローカル最適化しかできません。
グローバル最適化を行う為には、手順が増えますがLLVMの中間コードを作る必要があります。
Makefileを書く参考にでもなるかと思います。
$ clang -emit-llvm -c test1.c (プリプロセス、ビットコードコンパイル。test1.oができる。) $ clang -emit-llvm -c test2.c (プリプロセス、ビットコードコンパイル。test2.oができる。) $ llvm-nm test1.o test2.o test1.o: T main U swap U printf test2.o: T swap (シンボルのリストアップ。言語コンパイル時点では最適化しないのが流儀。) $ llvm-ld test1.o test2.o (リンク。面白いことにa.outとa.out.bcというファイルができる。 実はa.outはシェルスクリプトで、次のコマンドを呼び出す。) $ lli a.out.bc x = 2, y = 1 (ビットコードa.out.bcをJITコンパイラ付きランタイムで実行。) $ llvm-nm a.out.bc T main U printf (ビットコードのシンボルリストアップ。なんとswapが無くなってる!!!) $ llc a.out.bc (システムコンパイル。a.out.sができる。) $ clang a.out.s (ネイティブコードアセンブル、リンク、ロード。a.outが上書きされる。) $ nm a.out | grep -v " _" 0000000000601010 W data_start 0000000000400540 T main U printf@@GLIBC_2.2.5 (実行ファイルのシンボルリストアップ。色々内部シンボル出てくるけど非表示。) $ ./a.out x = 2, y = 1 (実行可能。)
実に面白い結果が得られました。
ビットコードなる中間言語コードのリンクをすることで、swap関数がインライン展開された模様です。
printf関数は独自のものに差し替えられていないようで、この辺が戦略の違いでしょうか。
LLVMを使う場合は言語ソースのコンパイル時点では最適化しないのが流儀だそうで、
シンボルを破壊せずデバッグしやすいし、より統計的に効率のいいグローバル最適化ができるって話です。
まだまだ実行ファイルのパフォーマンスに関しては圧倒的なアドバンテージは持っていないものの、
コンパイル速度やメモリ効率は既にGCCを凌駕しています。
また、使ってみると分かりますがエラーメッセージが非常に親切で分かりやすく、
今後の発展に大いに期待できそうです。
例えば、スタティックリンクするライブラリが巨大なもの(標準C++ライブラリやwxWidgets)であったとしても、
LLVMのビットコード形式でライブラリが用意されくれるようになれば、
リンカが必要シンボルだけをピックアップしてリンクしてくれるので、
リリースファイルが肥大化せずに済むんじゃないかなー。というかこれ本当に実現できそうだなー。
D言語も(あんまり活発じゃないけど)LLVMポーティングされ始めてるみたいだし、
この環境ならC++でも多少はイライラが解決するんじゃないかとも感じています。