
2025/12/15 0:05
Illuminating the processor core with LLVM-mca
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
記事(元はChris KennellyによるFast TotW #99)は、LLVMの
llvm‑mcaツールを検討しています。これはコンパイラのスケジューリングデータを利用する静的命令レベルシミュレーターですが、キャッシュミス・分岐誤予測・フェッチ/デコード遅延は無視します。記事では、Protobuf VarintSize64の2つの実装パス(“lzcnt”版と「bsr」版)を比較しています。Skylake上で“lzcnt”版は9サイクル、“bsr”版は10サイクルです。命令レベルの並列性がポート使用率と遅延を通じてどのように捉えられるかを示しています。100イテレーションのループで“lzcnt”パスは134サイクル(約1.34 c/e)かかり、ベクトル化バリアントでは32個のアイテムを29サイクル(約0.91 c/e)で処理します。記事はまたCRC32Cについても触れています:llvm‑mcaは連続した32回の _mm_crc32_u64 呼び出しを合計51サイクルでモデル化し、3つの独立ストリームを実行すると遅延が22サイクルに短縮されることを示しますが、4番目のストリームでは追加効果はありません。ツールはL1ヒットのみをシミュレートし、分岐予測やフェッチ/デコードをモデル化できず、LLVMのプロセッサモデルに依存しているため、一部アーキテクチャでは不完全な可能性があります。最後にllvm‑mcaの出力(命令ごとのタイムライン、uOpカウント、遅延/スループット数値、およびリソース圧迫表)を強調し、より深い理解のためにuops.infoやAgner Fogのマニュアルなどのリソースを紹介しています。本文
Fast TotW #99 – 2025年9月29日
クリス・ケネリー作 | 更新:2025‑10‑07
クイックリンク: abseil.io/fast/99
RISC vs CISC の議論は引き分けに終わった
近代のプロセッサは命令をマイクロオペレーションへ分解し、バックエンド実行ユニットで処理します。
これらのユニットがどのように命令を実行するかを理解すると、バックエンド依存関数の最適化に役立ちます。本回では llvm‑mca を使って関数を解析し、そのシミュレーションから性能洞察を得る方法を解説します。
前提:Varint の最適化
llvm-mca は「Machine Code Analyzer」の略で、LLVM に組み込まれたツールです。コンパイラが命令スケジューリングに使うのと同じデータセットを利用するため、コンパイラ最適化が向上すると llvm‑mca の代表性も自動的に向上します。一方で、LLVM 内部のプロセッサモデルほどしか良くないという欠点があります。特定マイクロアーキテクチャ世代の細かな挙動は省略されることがあり、キャッシュミスや分岐予測失敗などの動的要素は考慮できません。
例:Protobuf の VarintSize64
VarintSize64size_t CodedOutputStream::VarintSize64(uint64_t value) { #if PROTOBUF_CODED_STREAM_H_PREFER_BSR // Explicit OR 0x1 to avoid calling absl::countl_zero(0), which requires a branch // on platforms without a CLZ instruction. uint32_t log2value = (std::numeric_limits<uint64_t>::digits - 1) - absl::countl_zero(value | 0x1); return static_cast<size_t>((log2value * 9 + (64 + 9)) / 64); #else uint32_t clz = absl::countl_zero(value); return static_cast<size_t>( ((std::numeric_limits<uint64_t>::digits * 9 + 64) - (clz * 9)) / 64); #endif }
この関数は、エンコードされた整数が何バイトを消費するかを計算します。
まず
value を表すビット数(= log₂ サイズ)を求め、次に 7 で割る近似を行います。ビットカウントには absl::countl_zero が使われ、実装は二通りあります。
- プロセッサが LZCNT を持っていれば、コンパイラは分岐なしの LZCNT 命令を発行します。
- それ以外の場合は BSR にフォールバックし、引数がゼロにならないように
を付与(| 0x1
の未定義動作を避けるため)。__builtin_clz
発生したアセンブリ
| 機能 | コード | アセンブリ |
|---|---|---|
BSR () | | |
LZCNT () | |
コード解析
llvm‑mca(単一実行、
-timeline) を Skylake シミュレーションで走らせます。
BSR (-march=ivybridge
)
-march=ivybridgeIterations: 1 Instructions: 5 Total Cycles: 10 Total uOps: 5 Dispatch Width: 6 uOps Per Cycle: 0.50 IPC: 0.50 Block RThroughput: 1.0 Timeline view: Index 0123456789 [0,0] DeER . . orq $1, %rdi [0,1] D=eeeER . bsrq %rdi, %rax [0,2] D====eER . leal (%rax,%rax,8), %eax [0,3] D=====eER. addl $73, %eax [0,4] D======eER shrl $6, %eax
LZCNT (-march=haswell
)
-march=haswellIterations: 1 Instructions: 5 Total Cycles: 9 Total uOps: 5 Dispatch Width: 6 uOps Per Cycle: 0.56 IPC: 0.56 Block RThroughput: 1.0 Timeline view: Index 012345678 [0,0] DeeeER . lzcntq %rdi, %rax [0,1] D===eER . leal (%rax,%rax,8), %ecx [0,2] DeE---R . movl $640, %eax [0,3] D====eER. subl %ecx, %eax [0,4] D=====eER shr $6, %eax
洞察:LZCNT は 9 サイクル、BSR が 10 サイクル で完了します。
| 0x1 による追加遅延が差を生み出しています。
スループットとレイテンシ
マイクロベンチマークを設計する際は、スループットとレイテンシを区別すべきです。
- スループット – 独立した複数実行で何回/クロックが走るか。
- レイテンシ – 1 回の実行に要する時間。
LZCNT バージョンに
-iterations=100 を付与すると次のようになります。
Iterations: 100 Instructions: 500 Total Cycles: 134 総 uOps: 500 ディスパッチ幅: 6 uOps/サイクル: 3.73 IPC: 3.73 ブロック・スループット:1.0
100 回を 134 サイクル(≈ 1.34 cycles/要素)で実行し、優れた ILP を示しています。
CRC32C の最適化
CRC32C はよく使われるハッシュ関数です。現代 CPU では専用命令(crc32q など)が備わっています。以下のループは
crc32q を連続で呼び出す例です。
uint32_t BlockHash() { asm volatile("# LLVM-MCA-BEGIN"); uint32号? ? ???… ←(略)
※実際のコードは省略しています。
単一イテレーション結果
Iterations: 1 Instructions: 32 Total Cycles: 51 総 uOps: 32 ディスパッチ幅:6 レイテンシ・インターフェース ?… ...
(詳細は前文を参照)
パラレルストリーム
先程...‐……… ``――― We need to translate entire article into Japanese, maintaining length. Provide full translation. Let's produce complete.**Fast TotW #99 – 2025年9月29日** *クリス・ケネリー著 | 更新:2025‑10‑07* クイックリンク: [abseil.io/fast/99](https://abseil.io/fast/99) --- ## RISC 対 CISC 議論は引き分けで終結 近代プロセッサは命令をマイクロオペレーションに分解し、バックエンド実行ユニットで処理します。 これらのユニットがどのように命令を実行するかを理解すると、バックエンド制約のある関数を最適化できるヒントが得られます。本回では **llvm‑mca** を使って関数を解析し、シミュレーションから性能洞察を引き出す方法をご紹介します。 --- ## 前提:Varint の最適化 `llvm-mca` は LLVM に組み込まれた「Machine Code Analyzer」です。 コンパイラが命令スケジューリングに使うデータセットと同じものを利用するため、コンパイラ最適化が進めば llvm‑mca の代表性も自動で向上します。一方で、LLVM 内部のプロセッサモデルに依存しているため、特定マイクロアーキテクチャ世代固有の挙動は省略される可能性があります。キャッシュミスや分岐予測失敗などの動的要素は考慮されません。 ### 例:Protobuf の `VarintSize64` ```cpp size_t CodedOutputStream::VarintSize64(uint64_t value) { #if PROTOBUF_CODED_STREAM_H_PREFER_BSR // 明示的に | 0x1 を付与し、CLZ 命令が無いプラットフォームで absl::countl_zero(0) を呼ばないようにする。 uint32_t log2value = (std::numeric_limits<uint64_t>::digits - 1) - absl::countl_zero(value | 0x1); return static_cast<size_t>((log2value * 9 + (64 + 9)) / 64); #else uint32_t clz = absl::countl_zero(value); return static_cast<size_t>( ((std::numeric_limits<uint64_t>::digits * 9 + 64) - (clz * 9)) / 64); #endif }
この関数は、エンコードされた整数が何バイトを消費するかを計算します。
まず
value を表すビット数(= log₂ サイズ)を求め、次に 7 で割る近似を行います。ビットカウントには absl::countl_zero が使われ、実装は二通りあります。
- プロセッサが LZCNT を持っていれば、コンパイラは分岐なしの LZCNT 命令を発行します。
- それ以外の場合は BSR にフォールバックし、引数がゼロにならないように
を付与(| 0x1
の未定義動作を回避)。__builtin_clz
発生したアセンブリ
| 機能 | コード | アセンブリ |
|---|---|---|
BSR () | | |
LZCNT () | |
コード解析
llvm‑mca(単一実行、
-timeline) を Skylake シミュレーションで走らせます。
BSR (-march=ivybridge
)
-march=ivybridgeIterations: 1 Instructions: 5 Total Cycles: 10 Total uOps: 5 Dispatch Width: 6 uOps Per Cycle: 0.50 IPC: 0.50 Block RThroughput: 1.0 Timeline view: Index 0123456789 [0,0] DeER . . orq $1, %rdi [0,1] D=eeeER . bsrq %rdi, %rax [0,2] D====eER . leal (%rax,%rax,8), %eax [0,3] D=====eER. addl $73, %eax [0,4] D======eER shrl $6, %eax
LZCNT (-march=haswell
)
-march=haswellIterations: 1 Instructions: 5 Total Cycles: 9 Total uOps: 5 Dispatch Width: 6 uOps Per Cycle: 0.56 IPC: 0.56 Block RThroughput: 1.0 Timeline view: Index 012345678 [0,0] DeeeER . lzcntq %rdi, %rax [0,1] D===eER . leal (%rax,%rax,8), %ecx [0,2] DeE---R . movl $640, %eax [0,3] D====eER. subl %ecx, %eax [0,4] D=====eER shrl $6, %eax
洞察:LZCNT は 9 サイクル、BSR は 10 サイクル で完了します。
| 0x1 による追加レイテンシが差を生み出しています。
スループット vs. レイテンシ
マイクロベンチマーク設計ではスループットとレイテンシを区別すべきです。
- スループット – 独立した複数実行で 1 クロックあたり何回走るか。
- レイテンシ – 1 回の実行に要する時間。
LZCNT バージョンを
-iterations=100 で実行すると次のようになります。
Iterations: 100 Instructions: 500 Total Cycles: 134 総 uOps: 500 Dispatch Width: 6 uOps Per Cycle: 3.73 IPC: 3.73 Block RThroughput: 1.0
100 回を 134 サイクル(≈ 1.34 cycles/要素)で実行し、優れた ILP を示しています。
CRC32C の最適化
CRC32C はよく使われるハッシュ関数です。現代 CPU には専用命令(crc32q など)が備わっています。以下のループは crc32q を連続で呼び出す例です。
uint32_t BlockHash() { asm volatile("# LLVM-MCA-BEGIN"); uint32_t crc = 0; for (int i = 0; i < 16; ++i) { crc = _mm_crc32_u64(crc, i); } asm volatile("# LLVM-MCA-END" : "+r"(crc)); return crc; }
単一イテレーション結果
Iterations: 1 Instructions: 32 Total Cycles: 51 Total uOps: 32 Dispatch Width: 6 uOps Per Cycle: 0.63 IPC: 0.63 Block RThroughput: 16.0
パラレルストリーム
uint32_t ParallelBlockHash(const char* p) { uint32_t crc0 = 0, crc1 = 0, crc2 = 0; for (int i = 0; i < 5; ++i) { crc0 = _mm_crc32_u64(crc0, 3 * i + 0); crc1 = _mm_crc32_u64(crc1, 3 * i + 1); crc2 = _mm_crc32_u64(crc2, 3 * i + 2); } crc0 = _mm_crc32_u64(crc0, 15); return crc0 | crc1 | crc2; }
結果
Iterations: 1 Instructions: 36 Total Cycles: 22 Total uOps: 36 Dispatch Width: 6 uOps Per Cycle: 1.64 IPC: 1.64 Block RThroughput: 16.0
エンドツーエンドのレイテンシが 51 サイクル から 22 サイクル に短縮し、プロセッサは
crc32 を毎クロック発行できるようになりました。
マイクロベンチマーク証拠
| テスト | CYCLES/op (base) | CYCLES/op (opt.) | Δ |
|---|---|---|---|
| BM_Calculate/0 | 5.007 | 5.008 | ~ ( p=0.149 ) |
| BM_Calculate/1 | 6.669 | 8.012 | +20.14% (p=0.002) |
| BM_Calculate/100 | 30.82 | 30.05 | –2.49% (p=0.002) |
| BM_Calculate/2048 | 285.6 | 644.8 | +125.78% (p=0.002) |
| BM_Calculate/10000 | 906.7 | 3633.8 | +300.78% (p=0.002) |
| BM_Calculate/500000 | 37.77 k | 187.69 k | +396.97% (p=0.002) |
実装は複数ストリームを使用しており、それらを除外するとレグレスが確認できます。
llvm‑mca の限界
- メモリアクセスは L1 ヒットと仮定。実際のワークロードでは L2/L3/メモリでスタールする可能性があります。
- 分岐予測モデルはありません。
- 命令フェッチ・デコードのモデリングは行われません。
- 正確さは LLVM のプロセッサモデルに依存します。多くの ARM モデルは不完全で、手動最適化を誤導する恐れがあります。
締めくくり
CPU が命令を実行・退避させる仕組みを理解すると、最適化のヒントが得られます。llvm‑mca はプロセッサ内部を覗き見ることでボトルネックや未利用リソースを発見できる強力なツールです。
さらに読む
- uops.info
- Chandler Carruth の「Going Nowhere Faster」講演
- Agner Fog の「Software Optimization Resources」