はじめに
CPUの性能について考える際には、命令数や IPC(Instructions Per Cycle) といった数値を着目しがちですが、実際の性能は命令間の依存関係、実行ユニット (ALU, Load/Store, FPU) の競合、パイプラインの発行幅 (issue width) といったマイクロアーキテクチャ依存の要素に強く影響されます。
このような要素を考慮する際に役立つのが llvm-mca です。これはLLVMに含まれる静的マイクロアーキテクチャ解析ツールで、アセンブリ命令列を入力すると、
- 想定CPU上での実行サイクル
- IPC (理論値)
- 各実行ポートへの負荷 (Resource Pressure)
- 命令ごとのレイテンシと依存関係
- 発行から完了までのタイムライン
のシミュレーション結果を表示できます。
静的解析なので、実機で実行せずに命令列のボトルネックを解析することが可能です。
- llvm-mca: LLVM Machine Code Analyzer [1]
llvm-mcaのインストール
LLVMのツールチェインに含まれるツールのため、Homebrewでllvmをインストールすることで使えるようになります。コマンドの実行にはパスを通しておく必要があります。
$ brew install llvm
$ export PATH="$(brew --prefix llvm)/bin:$PATH"
llvm-mcaで依存チェーンによる性能低下を見てみる
以下のような命令列は、前の命令の結果を次の命令が利用するため、パイプラインが並列実行できません。
このような命令列は、仮に発行幅が3であっても並列にはならず、レイテンシで詰まる状態になります。
add x0, x0, x1
add x0, x0, x1
add x0, x0, x1
この命令列をllvm-mcaに入力した結果を順に示します。
実行コマンド
echo 'add x0, x0, x1\n add x0, x0, x1\n add x0, x0, x1'
| llvm-mca -timelime -mtriple=aarch64 -mcpu=neoverse-n1
今回はNeoverse-N1というCPUコアモデルでシミュレーションします。
Summary
Iterations: 100
Instructions: 300
Total Cycles: 303
Total uOps: 300
Dispatch Width: 3
uOps Per Cycle: 0.99
IPC: 0.99
Block RThroughput: 1.0
- Iterations:100 / Instructions:300
3命令のブロックを100回繰り返す前提で評価します。(合計300命令) - Dispatch Width:3
Neoverse-N1モデルでは、1サイクルに最大3uopsをディスパッチできるという前提です。 - IPC:0.99 / uOps Per Cycle:0.99
実測ではなく、理想モデル上で、平均するとほぼ1命令/サイクルになっています。 - Block RThroughput:1.0
この3命令ブロックを定常状態で回したとき、ブロックあたり必要サイクルが1.0という推定です。
Instruction Info
Instruction Info:
[1]: #uOps
[2]: Latency
[3]: RThroughput
[4]: MayLoad
[5]: MayStore
[6]: HasSideEffects (U)
[1] [2] [3] [4] [5] [6] Instructions:
1 1 0.33 add x0, x0, x1
1 1 0.33 add x0, x0, x1
1 1 0.33 add x0, x0, x1
#uOps=1
addは1uop扱いです。
Latency=1
依存する次の命令に結果を渡せるまでが1サイクルになります。(ALU結果の依存レイテンシが1)
RThroughput=0.33
これは理想的に独立なaddを並べたときの最短感覚が0.33 cycles/instという意味合いです。つまり、依存がなければ、発行幅や実行ポートが許す範囲でaddはかなり高密度に流すことができる想定です。
今回IPCが1付近なのは、依存チェーンや実行資源の使い方が効いて理想に到達していないため。
Resources
Resources:
[0] - N1UnitB
[1.0] - N1UnitD
[1.1] - N1UnitD
[2.0] - N1UnitL
[2.1] - N1UnitL
[3] - N1UnitM
[4.0] - N1UnitS
[4.1] - N1UnitS
[5] - N1UnitV0
[6] - N1UnitV1
Resource pressure per iteration:
[0] [1.0] [1.1] [2.0] [2.1] [3] [4.0] [4.1] [5] [6]
- - - - - - 1.50 1.50 - -
Resource pressure by instruction:
[0] [1.0] [1.1] [2.0] [2.1] [3] [4.0] [4.1] [5] [6] Instructions:
- - - - - - 0.50 0.50 - - add x0, x0, x1
- - - - - - 0.50 0.50 - - add x0, x0, x1
- - - - - - 0.50 0.50 - - add x0, x0, x1
リソース情報では、N1UnitSがこのモデル上の整数系の実行リソースの一部を表しています。
Resource pressure per iterationにおいて、
[4.0] 1.50, [4.1] 1.50
であることから、1 iterationあたり
N1UnitS[4.0]を1.5, N1UnitS[4.1]を1.5
サイクル分占有することが読み取れます。
addは1発で
N1UnitS[4.0]を0.5, N1UnitS[4.1]を0.5
となっているので、addはS系の2本にまたがって均等に割り振れるという表現になります。結果としては以下の見方ができます。
- 各addがN1UnitS[4.0]とN1UnitS[4.1]に0.5ずつ圧をかける
- 3本あるので1 iterationあたり1.5/1.5になる
この命令列は整数演算のS系ユニットを使っていて、そこは空いているが、依存チェーンで並列度が出ず、結果として1IPCに落ち着いている
Timelime
Timeline view:
0123456789 012
Index 0123456789 0123456789
[0,0] DeER . . . . . . . add x0, x0, x1
[0,1] D=eER. . . . . . . add x0, x0, x1
[0,2] D==eER . . . . . . add x0, x0, x1
[1,0] .D==eER . . . . . . add x0, x0, x1
[1,1] .D===eER . . . . . . add x0, x0, x1
[1,2] .D====eER . . . . . . add x0, x0, x1
[2,0] . D====eER. . . . . . add x0, x0, x1
[2,1] . D=====eER . . . . . add x0, x0, x1
[2,2] . D======eER . . . . . add x0, x0, x1
[3,0] . D======eER . . . . . add x0, x0, x1
[3,1] . D=======eER . . . . . add x0, x0, x1
[3,2] . D========eER. . . . . add x0, x0, x1
[4,0] . D========eER . . . . add x0, x0, x1
[4,1] . D=========eER . . . . add x0, x0, x1
[4,2] . D==========eER . . . . add x0, x0, x1
[5,0] . D==========eER . . . . add x0, x0, x1
[5,1] . D===========eER. . . . add x0, x0, x1
[5,2] . D============eER . . . add x0, x0, x1
[6,0] . .D============eER . . . add x0, x0, x1
[6,1] . .D=============eER . . . add x0, x0, x1
[6,2] . .D==============eER . . . add x0, x0, x1
[7,0] . . D==============eER. . . add x0, x0, x1
[7,1] . . D===============eER . . add x0, x0, x1
[7,2] . . D================eER . . add x0, x0, x1
[8,0] . . D================eER . . add x0, x0, x1
[8,1] . . D=================eER . . add x0, x0, x1
[8,2] . . D==================eER. . add x0, x0, x1
[9,0] . . D==================eER . add x0, x0, x1
[9,1] . . D===================eER. add x0, x0, x1
[9,2] . . D====================eER add x0, x0, x1
Average Wait times (based on the timeline view):
[0]: Executions
[1]: Average time spent waiting in a scheduler's queue
[2]: Average time spent waiting in a scheduler's queue while ready
[3]: Average time elapsed from WB until retire stage
[0] [1] [2] [3]
0. 10 10.0 0.1 0.0 add x0, x0, x1
1. 10 11.0 0.0 0.0 add x0, x0, x1
2. 10 12.0 0.0 0.0 add x0, x0, x1
10 11.0 0.0 0.0 <total>
timelimeはCPUモデルにもよりますが、大体は以下の解釈になります。
- D: Dispatch, フロントエンド->バックエンドへの投入
- e: execute, 実行ステージ
- E: Execute, 完了・実行中
- R: Retire, リタイア・コミット
=はそのステージに居続けている状態を意味しています。
この命令列はaddにx0の結果を待つという依存関係があるので、命令列を連ねた場合にも前の命令の実行が完了する(E)までは次の命令は順番待ちをしています。
次に、以下の命令列の結果も見てみます。
この命令列は、依存関係がなく、発行幅に応じて並列化できます。
$ echo 'add x2, x2, x3\n add x4, x4, x5\n add x6, x6, x7'
| llvm-mca -timeline -mtriple=aarch64 -mcpu=neoverse-n1
Iterations: 100
Instructions: 300
Total Cycles: 103
Total uOps: 300
Dispatch Width: 3
uOps Per Cycle: 2.91
IPC: 2.91
Block RThroughput: 1.0
Instruction Info:
[1]: #uOps
[2]: Latency
[3]: RThroughput
[4]: MayLoad
[5]: MayStore
[6]: HasSideEffects (U)
[1] [2] [3] [4] [5] [6] Instructions:
1 1 0.33 add x2, x2, x3
1 1 0.33 add x4, x4, x5
1 1 0.33 add x6, x6, x7
Resources:
[0] - N1UnitB
[1.0] - N1UnitD
[1.1] - N1UnitD
[2.0] - N1UnitL
[2.1] - N1UnitL
[3] - N1UnitM
[4.0] - N1UnitS
[4.1] - N1UnitS
[5] - N1UnitV0
[6] - N1UnitV1
Resource pressure per iteration:
[0] [1.0] [1.1] [2.0] [2.1] [3] [4.0] [4.1] [5] [6]
- - - - - 1.00 1.00 1.00 - -
Resource pressure by instruction:
[0] [1.0] [1.1] [2.0] [2.1] [3] [4.0] [4.1] [5] [6] Instructions:
- - - - - - - 1.00 - - add x2, x2, x3
- - - - - - 1.00 - - - add x4, x4, x5
- - - - - 1.00 - - - - add x6, x6, x7
Timeline view:
012
Index 0123456789
[0,0] DeER . . . add x2, x2, x3
[0,1] DeER . . . add x4, x4, x5
[0,2] DeER . . . add x6, x6, x7
[1,0] .DeER. . . add x2, x2, x3
[1,1] .DeER. . . add x4, x4, x5
[1,2] .DeER. . . add x6, x6, x7
[2,0] . DeER . . add x2, x2, x3
[2,1] . DeER . . add x4, x4, x5
[2,2] . DeER . . add x6, x6, x7
[3,0] . DeER . . add x2, x2, x3
[3,1] . DeER . . add x4, x4, x5
[3,2] . DeER . . add x6, x6, x7
[4,0] . DeER . . add x2, x2, x3
[4,1] . DeER . . add x4, x4, x5
[4,2] . DeER . . add x6, x6, x7
[5,0] . DeER . . add x2, x2, x3
[5,1] . DeER . . add x4, x4, x5
[5,2] . DeER . . add x6, x6, x7
[6,0] . .DeER. . add x2, x2, x3
[6,1] . .DeER. . add x4, x4, x5
[6,2] . .DeER. . add x6, x6, x7
[7,0] . . DeER . add x2, x2, x3
[7,1] . . DeER . add x4, x4, x5
[7,2] . . DeER . add x6, x6, x7
[8,0] . . DeER. add x2, x2, x3
[8,1] . . DeER. add x4, x4, x5
[8,2] . . DeER. add x6, x6, x7
[9,0] . . DeER add x2, x2, x3
[9,1] . . DeER add x4, x4, x5
[9,2] . . DeER add x6, x6, x7
Average Wait times (based on the timeline view):
[0]: Executions
[1]: Average time spent waiting in a scheduler's queue
[2]: Average time spent waiting in a scheduler's queue while ready
[3]: Average time elapsed from WB until retire stage
[0] [1] [2] [3]
0. 10 1.0 0.1 0.0 add x2, x2, x3
1. 10 1.0 0.1 0.0 add x4, x4, x5
2. 10 1.0 0.1 0.0 add x6, x6, x7
10 1.0 0.1 0.0 <total>
こちらはaddに依存関係がないので、timelineを見ると並列実行できていることが分かります。
IPC=2.91とほぼ3に近い数値となっています。
終わりに
今回はllvm-mcaの出力について確認し、性能分析の解釈を纏めてみました。
llvm-mcaの強みと注意点は以下になります。
- 強み
- 実機がなくても解析可能
- CPUモデルごとの差が比較可能
- パイプラインの挙動を可視化できる
- コンパイラ最適化の効果を検証できる
- 注意点
- メモリ階層(キャッシュミスなど)は正確に再現しない
- 分岐予測の現実挙動は単純化される
- CPUモデルやレイテンシはLLVMに登録されているもののみ利用できる
- 実機との差はあり得る