はじめに
目的
この記事は、Crystalのコンパイラの遅さを歯がゆく思っているkojix2がCrystalとLLVMまわりを自分でもっと観察したいと考えて、初心者向けのチートシートとして作成しました。
作成方法と検証
この記事は生成AIを繰り返し用いて作成しています。kojix2 の書いた下書きを元に、ChatGPT, Claude という2つのAIに記事を何回か通して作成しました。(まさにこの記事のテーマであるPASSによる最適化みたいですね~)
さらに記事に記載されているコマンドが実際に動作するかについてAgentを用いてUbuntu環境で一通り検証を行いました。
環境セットアップ
バージョン確認
この記事では llvm-20 に基づいて書かれています。
llc --version
crystal --version
Ubuntu/APT でのコマンド名
LLVM 20 をインストールした場合:
llvm-as-20
llvm-dis-20
llc-20
opt-20
短縮する方法:
# aliasを使う
alias llc='llc-20'
alias opt='opt-20'
# または update-alternatives を使う
sudo update-alternatives --install /usr/bin/llc llc /usr/bin/llc-20 100
macOS/Homebrew での注意点
Homebrewでインストールした場合、PATHが通っていない可能性があります:
export PATH="$(brew --prefix llvm)/bin:$PATH"
ファイルフォーマット
| フォーマット | 拡張子 | 説明 |
|---|---|---|
| Crystal source | .cr |
Crystalのソースコード |
| LLVM IR (text) | .ll |
人間が読めるLLVM中間表現 |
| LLVM bitcode | .bc |
バイナリ形式のLLVM IR |
| Assembly | .s |
ターゲット依存のアセンブリ |
| Object file | .o |
オブジェクトファイル |
| Executable | (なし) | 実行ファイル |
やりたいこと別コマンド
コンパイル・変換
Crystal → Assembly
crystal build --emit asm --prelude empty test.cr
crystal build --release --emit asm test.cr # 最適化あり
Crystal → LLVM IR
crystal build --emit llvm-ir --prelude empty test.cr
crystal build --debug --emit llvm-ir test.cr # デバッグ情報付き
LLVM IR (.ll) → Assembly (.s)
llc --relocation-model=pic test.ll
llc -asm-verbose test.ll # コメント多め
LLVM IR (.ll) → Bitcode (.bc)
llvm-as test.ll
Bitcode (.bc) → LLVM IR (.ll)
llvm-dis test.bc
llvm-dis -o test.ll test.bc # 出力先指定
Assembly (.s) → Executable
clang test.s
clang test.s -o test # 出力ファイル名指定
clang test.s -levent -lgc -lm # ライブラリ指定
Bitcode を直接実行
lli test.bc
注意: Crystalのランタイム依存関係(GCなど)がないため、実行時エラーが発生する可能性があります。
最適化
最適化レベルを指定してビルド
crystal build --release test.cr # -O3 --single-module
crystal build --no-debug test.cr
LLVM IR に最適化をかける
opt -S -O0 test.ll > O0.ll
opt -S -O1 test.ll > O1.ll
opt -S -O2 test.ll > O2.ll
opt -S -O3 test.ll > O3.ll
特定のパスだけ適用
opt -S -p instcombine test.ll > ic.ll
opt -S -p inline test.ll > inlined.ll
opt -S -p 'function(mem2reg)' test.ll > readable.ll # メモリ→レジスタ化
特定の最適化を無効化
opt -S -O3 -disable-loop-unrolling test.ll > no_unroll.ll
最適化パイプラインの表示
opt -O3 -print-pipeline-passes test.ll -disable-output
最適化前後の差分確認
llvm-diff O0.ll O3.ll
ターゲット指定でコード生成
llc -march=x86-64 -mcpu=skylake test.ll
llc -mtriple=aarch64-apple-darwin test.ll
解析・デバッグ
コンパイル時間を測定
time crystal build test.cr
crystal build --stats test.cr # 統計
LLVM 最適化パスの時間を測定
opt -time-passes -O3 test.ll -disable-output
opt -disable-output -time-passes -stats -O3 test.ll
コード生成(llc)の時間を測定
llc -time-passes test.ll
パス適用の詳細ログ
opt -O3 -debug-pass=Structure test.ll -disable-output 2>&1 | head -100
特定の最適化のデバッグログ
# 要: -DLLVM_ENABLE_ASSERTIONS=ON でビルドされたLLVM
opt -S -O3 -debug-only=instcombine test.ll 2>&1 | less
Bitcode の統計情報
llvm-bcanalyzer test.bc
llvm-bcanalyzer -dump test.bc | less
IR の検証
opt -p verify test.ll -disable-output
-p は -passes= の短縮形です。
関数の抽出
llvm-extract -func=fibonacci test.ll -o foo.ll
コードサイズの確認
wc -l test.ll O3.ll # 行数比較
llvm-size test.o # バイナリサイズ
llvm-size test.o O3.o # 最適化前後の比較
シンボル情報の確認
llvm-nm test.o
llvm-readelf -s test.o
llvm-objdump -t test.o
ディスアセンブル
llvm-objdump -d a.out | less
llvm-objdump -d -C a.out # C++デマングル付き
インライン化の確認
opt -S -p inline -print-after=inline test.ll -disable-output
可視化
Control Flow Graph (CFG) の生成
opt -p dot-cfg test.ll
dot -Tpng <generated.dot> -o foo.png
注: CFG ファイルのパスに特殊文字が含まれる場合、生成に失敗することがあります。
Call Graph の生成
opt -p dot-callgraph test.ll
dot -Tpng test.ll.callgraph.dot -o callgraph.png
プロファイリング
PGO (Profile-Guided Optimization)
# 1. インストルメント付きビルド
crystal build --emit llvm-ir test.cr
clang -fprofile-instr-generate test.ll -o test
# 2. 実行してプロファイル収集
./test
# default.profraw が生成される
# 3. プロファイルデータのマージ
llvm-profdata merge -output=default.profdata default.profraw
# 4. プロファイルを使って最適化
clang -fprofile-instr-use=default.profdata test.ll -o test_opt
注: Crystalランタイムの依存関係により、リンク時に失敗する可能性があります。
高度なテクニック
並列リンク (LLDリンカー使用)
crystal build --link-flags="-fuse-ld=lld -Wl,--threads=8" test.cr
プリリュードなしビルド (コンパイル時間短縮)
crystal build --prelude=empty test.cr
トラブルシューティング
「なぜこの最適化がされない?」
- 最適化前後のIRを比較:
llvm-diff before.ll after.ll - パスのログを確認:
opt -O3 -debug-pass=Structure test.ll -disable-output 2>&1 | head -100 - 特定パスのデバッグ:
opt -debug-only=instcombine test.ll(要assertions)
「どのパスが遅い?」
opt -time-passes -O3 test.ll -o /dev/null 2>&1 | sort -k2 -rn | head -20
「コードサイズが大きすぎる」
# サイズ確認
llvm-size a.out
llvm-objdump -d a.out | wc -l
# サイズ最適化ビルド
crystal build --release test.cr
opt -Oz test.ll -o test_opt.ll
「デバッグシンボルを確認したい」
llvm-dwarfdump a.out
llvm-symbolizer --obj=a.out
よく使うコマンド組み合わせ
パターン1: Crystalコードの最適化効果を確認
# 最適化なし
crystal build --emit llvm-ir test.cr
opt -S -O0 test.ll > O0.ll
# 最適化あり
opt -S -O3 test.ll > O3.ll
# 差分確認
llvm-diff O0.ll O3.ll
wc -l O0.ll O3.ll
パターン2: 特定関数の最適化を深掘り
# 1. 関数を抽出
llvm-extract -func=fibonacci test.ll -o func.ll
# 2. 最適化前後を比較
opt -S -O0 func.ll > func_O0.ll
opt -S -O3 func.ll > func_O3.ll
llvm-diff func_O0.ll func_O3.ll
# 3. Assembly確認
llc func_O3.ll
cat func_O3.s
パターン3: コンパイル時間のボトルネック調査
# Crystal側
crystal build --stats --verbose test.cr
# LLVM最適化側
opt -time-passes -O3 test.ll -disable-output 2>&1 | tee opt_time.log
# コード生成側
llc -time-passes test.ll 2>&1 | tee llc_time.log