工具链实践——llvm-mca
实践流程
https://llvm.org/docs/CommandGuide/llvm-mca.html
llvm-mca 是一款性能分析工具,它利用 LLVM 中可用的信息(例如调度模型)来静态测量特定 CPU 上机器码的性能。性能以吞吐量以及处理器资源消耗来衡量。此工具的主要目标不仅是预测代码在目标机器上运行时的性能,还包括帮助诊断潜在的性能问题。分析和报告风格借鉴了英特尔的 IACA 工具。
基本使用方法:把 clang foo.c -O2 --target=x86_64 -S -o - | llvm-mca -mcpu=btver2
汇编代码直接输入给 llvm-mca。调度模型不仅用于计算指令延迟和吞吐量,还用于了解可用的处理器资源以及如何模拟它们。根据设计,llvm-mca 进行的分析质量不可避免地受到 LLVM 中调度模型质量的影响。
特定选项
-dispatch=<width>
:为处理器指定不同的调度宽度-register-file-size=<size>
:此标志限制用于寄存器重命名目的的物理寄存器的数量(0 表示不限数量)-iterations=<number of iterations>
:指定要运行的迭代次数。-timeline
:启用时间线视图-scheduler-stats
:启用额外的调度程序统计信息。此视图收集并分析指令发出事件。默认情况下禁用此视图。-dispatch-stats
:启用额外的调度统计信息。此视图收集并分析指令调度事件,以及静态/动态调度停顿事件。默认情况下禁用此视图。-instruction-info
:启用指令信息视图。默认情况下启用此选项。-all-stats
: 打印所有硬件统计信息。这将启用与调度逻辑、硬件调度程序、寄存器文件和退休控制单元相关的额外统计信息。默认情况下禁用此选项。-all-views
:启用所有视图。--bottleneck-analysis
:==自动化瓶颈分析。==原理
llvm-mca 以汇编代码(一般是代码片段!)作为输入。在现有 LLVM 目标汇编解析器的帮助下,汇编代码被解析为一系列 MCInst。然后,由Pipeline
模块分析解析后的 MCInst 序列以生成性能报告。Pipeline 模块在迭代循环中模拟机器代码序列的执行(默认值为 100)。在此过程中,==管道收集大量与执行相关的统计信息==。在此过程结束时,管道生成并打印从收集的统计信息生成的报告。LLVM-MCA 不考虑完整的 CPU 前端, 即没有取码和译码环节
基础实践
先书写一个简单的 C++函数 (GPT 写的):
#include <vector> std::vector<int> findPrimes(int start, int end) { std::vector<int> primes; for (int i = start; i <= end; ++i) { if (i > 1) { bool is_prime = true; for (int j = 2; j * j <= i; ++j) { if (i % j == 0) { is_prime = false; break; } } if (is_prime) { primes.push_back(i); } } } return primes; }
然后使用 llvm 工具链的 clang++先编译为汇编文件并将结果传给 llvm-mca:
clang++ -S -O3 test.cpp -o - | llvm-mca | head -10
head -10
是为了只打印头部的重要信息,我的 MacBookPro M3 Max内容如下:Iterations: 100 Instructions: 19200 Total Cycles: 11355 Total uOps: 22800 Dispatch Width: 6 uOps Per Cycle: 2.01 IPC: 1.69 Block RThroughput: 54.5
他们分别表示:
- Iterations:仿真负载的模拟运行次数
- Instructions:指令数
- Cycles:总周期数
- uOps:微操作数量(例如 DIV 会由多个μOp 组成,尤其是 CISC 会拆分为多个类 RISC 的μOps)
- Dispatch Width:发射宽度,有多少个μOps 会在 1 个 cycle 内被发出(超标量)
- $\text{uOps Per Cycle} = \frac{\text{Total uOps}}{\text{Total Cycles}}$
- $\text{IPC} = \frac{\text{Instructions}}{\text{Total Cycles}}$
- $\text{Block RThroughput} = \frac{\text{Total Cycles}}{\text{Iterations}}$ :一次迭代需要的周期数,通常在比较两份代码时,可以直接看
Block RThroughput
,这就是一般意义上的性能指标(谁跑得快)。从上述分析我们其实已经可以粗略得知,IPC 远远没达到理想值,可能存在访存延迟、依赖和流水线停顿。
高级实践
现在我们用一个更简单的例子(为了防止打印太多内容)可以来详细查看一下完整版的输出内容,我们尽量一次性包含所有的命令行选项:
int compute(int a, int b) { int x = a + b; // Data dependency 1 int y = x * 2; // Data dependency 2 if (y > 10) { // Control flow (if condition) return y - 10; } return y; }
clang++ -S -O3 test.cpp -o - | llvm-mca --timeline --instruction-info --all-stats --bottleneck-analysis --dispatch-stats --all-views --show-encoding > anaylsis.txt
这个例子就比较简单了,我们来拆分一下他打印的内容:
基础性能总结
Iterations: 100 Instructions: 600 Total Cycles: 503 Total uOps: 600 Dispatch Width: 6 uOps Per Cycle: 1.19 IPC: 1.19 Block RThroughput: 1.8
性能瓶颈分析
--bottleneck-analysis
在此起作用:Cycles with backend pressure increase [ 89.86% ] Throughput Bottlenecks: Resource Pressure [ 0.00% ] Data Dependencies: [ 89.86% ] - Register Dependencies [ 89.86% ] - Memory Dependencies [ 0.00% ] Critical sequence based on the simulation: Instruction Dependency Information +----< 4. csel w0, w10, w9, gt | | < loop carried > | +----> 0. add w8, w1, w0 ## REGISTER dependency: w0 | 1. lsl w9, w8, #1 | 2. sub w10, w9, #10 +----> 3. cmp w8, #5 ## REGISTER dependency: w8 +----> 4. csel w0, w10, w9, gt ## REGISTER dependency: nzcv | 5. ret | | < loop carried > | +----> 0. add w8, w1, w0 ## REGISTER dependency: w0
通过以上内容我们可以得知:
- 89.86%的周期有“后端压力”,意味着指令执行较慢,多数因为有依赖和资源导致的停顿
- 没有资源压力(0.00%),意味着所有执行单元还没被全部利用,即还是上述依赖关系导致的
- 数据依赖也是 89.86%,即所有压力都是寄存器的数据依赖导致的。
- 没有内存压力(0.00%),即 load/store 的访存操作对性能没有影响。
在上述这段关键依赖指令序列中,显然我们已经知道了注释里写的 REGISTER DEPENDENCY,即几乎每条指令都对上一条的某个寄存器存在依赖关系。指令分析
--instruction-info
和--show-encoding
在此起作用:
这里也可以重点看[3],本质还是把基础性能总结的内容详细打了出来。Instruction Info: [1]: #uOps [2]: Latency [3]: RThroughput [4]: MayLoad [5]: MayStore [6]: HasSideEffects (U) [7]: Encoding Size [1] [2] [3] [4] [5] [6] [7] Encodings: Instructions: 1 2 1.00 4 28 00 00 0b add w8, w1, w0 1 1 0.50 4 09 79 1f 53 lsl w9, w8, #1 1 1 0.25 4 2a 29 00 51 sub w10, w9, #10 1 1 0.25 4 1f 15 00 71 cmp w8, #5 1 1 0.25 4 40 c1 89 1a csel w0, w10, w9, gt 1 0 1.00 U 4 c0 03 5f d6 ret
发射数据
--dispatch-stats
在此起作用:
这一段分析了为什么 CPU 停顿以及每周期发射的微操作。Dynamic Dispatch Stall Cycles: RAT - Register unavailable: 0 RCU - Retire tokens unavailable: 0 SCHEDQ - Scheduler full: 442 (87.9%) LQ - Load queue full: 0 SQ - Store queue full: 0 GROUP - Static restrictions on the dispatch group: 0 USH - Uncategorised Structural Hazard: 0 Dispatch Logic - number of cycles where we saw N micro opcodes dispatched: [# dispatched], [# cycles] 0, 138 (27.4%) 1, 176 (35.0%) 2, 177 (35.2%) 4, 1 (0.2%) 6, 11 (2.2%)
- RAT:Register Allocation Table,代表 CPU 用完了物理寄存器,因此推迟指令执行
- RCU (Retire Control Unit),太多指令准备退役,导致后端压力(建议学习保留站知识)
- SCHEDQ (Scheduler full),指令调度器已经满载,无法支持新的指令装入
- LQ (Load Queue full),超标量单元的“load缓冲”已满,无法支持更多加载指令。
- SQ:同上,存储指令
- GROUP (Static dispatch restrictions),CPU 在一起发射特定指令组合时受限
- USH (Uncategorized Structural Hazard),硬件单元的限制阻碍了指令的执行
主要瓶颈在于 Scheduler full,即 CPU 没能快速分发某些命令,导致后续必须等待。可能的解决措施可以是减少指令依赖、增加 ILP 等等。
在下面那部分比较直观,展示的是
调度器与资源使用
--all-stats
在此起作用:
Schedulers - number of cycles where we saw N micro opcodes issued:
[# issued], [# cycles]
0, 101 (20.1%)
1, 206 (41.0%)
2, 194 (38.6%)
3, 2 (0.4%)
Scheduler's queue usage:
[1] Resource name.
[2] Average number of used buffer entries.
[3] Maximum number of used buffer entries.
[4] Total number of buffer entries.
[1] [2] [3] [4]
CyUnitB 0 1 24
CyUnitI 45 48 48
CyUnitID 0 0 16
CyUnitIM 0 0 32
CyUnitIS 17 20 24
CyUnitLS 0 0 28
CyUnitV 0 0 48
CyUnitVC 0 0 16
CyUnitVD 0 0 16
CyUnitVM 0 0 32
上一部分是分发,这一块就是执行操作,代表指令的执行单元的使用频度。
下面这一块内容表示:超标量单元的并行执行单元的个数,可以看出 Mac 还是非常夸张。
Resource Name | Meaning (Execution Unit) | Avg Used | Max Used | Total Capacity |
---|---|---|---|---|
CyUnitB | Branch unit (e.g., branch prediction, jumps) | 0 | 1 | 24 |
CyUnitI | Integer ALU (General-purpose integer execution) | 45 | 48 | 48 |
CyUnitID | Integer Divide Unit | 0 | 0 | 16 |
CyUnitIM | Integer Multiply Unit | 0 | 0 | 32 |
CyUnitIS | Integer Store/Shift Unit | 17 | 20 | 24 |
CyUnitLS | Load/Store Unit (memory access) | 0 | 0 | 28 |
CyUnitV | Vector Unit (SIMD/AVX) | 0 | 0 | 48 |
CyUnitVC | Vector Compute Unit (General vector operations) | 0 | 0 | 16 |
CyUnitVD | Vector Divide Unit | 0 | 0 | 16 |
CyUnitVM | Vector Multiply Unit | 0 | 0 | 32 |
退役控制单元
来源于 --all-stats
:
这个模块作用是按序提交已经处理完的指令(这些指令没有错误!)
Retire Control Unit - number of cycles where we saw N instructions retired:
[# retired], [# cycles]
0, 103 (20.5%)
1, 200 (39.8%)
2, 200 (39.8%)
重排序缓冲利用情况(ROB)
来源于 --all-stats
:
Total ROB Entries: 192
Max Used ROB Entries: 61 (31.8%)
Average Used ROB Entries per cy: 56 (29.2%)
- 重排序缓冲存储的是已经乱序执行完毕的指令,他们正等待给 RCU 处理。
寄存器文件数据
来源于--all-stats
:Total number of mappings created: 500 Max number of mappings used: 51
- 第一条意味着存在(硬件)寄存器重命名的数量
- 最高有 51 个寄存器同时在使用
寄存器重命名(Register Renaming) 是现代 CPU 为了提高指令级并行性(ILP)和减少数据依赖而采用的一种硬件优化技术。在 乱序执行(Out-of-Order Execution, OoOE) 的 CPU 中,逻辑寄存器的数量有限,但CPU 实际上有更多的物理寄存器。寄存器重命名的核心思想是 将有限的逻辑寄存器映射到更丰富的物理寄存器,从而消除假依赖(False Dependency)。
资源使用情况
来源于
--resource-pressure
:Resources: [0.0] - CyUnitB [0.1] - CyUnitB [1] - CyUnitBR [2.0] - CyUnitFloatDiv [2.1] - CyUnitFloatDiv [3.0] - CyUnitI [3.1] - CyUnitI [3.2] - CyUnitI [3.3] - CyUnitI [4] - CyUnitID [5] - CyUnitIM [6.0] - CyUnitIS [6.1] - CyUnitIS [7] - CyUnitIntDiv [8.0] - CyUnitLS [8.1] - CyUnitLS [9.0] - CyUnitV [9.1] - CyUnitV [9.2] - CyUnitV [10] - CyUnitVC [11] - CyUnitVD [12.0] - CyUnitVM [12.1] - CyUnitVM Resource pressure per iteration: [0.0] [0.1] [1] [2.0] [2.1] [3.0] [3.1] [3.2] [3.3] [4] [5] [6.0] [6.1] [7] [8.0] [8.1] [9.0] [9.1] [9.2] [10] [11] [12.0] [12.1] 0.50 0.50 1.00 - - 1.97 1.53 1.96 1.54 - - 1.00 2.00 - - - - - - - - - - Resource pressure by instruction: [0.0] [0.1] [1] [2.0] [2.1] [3.0] [3.1] [3.2] [3.3] [4] [5] [6.0] [6.1] [7] [8.0] [8.1] [9.0] [9.1] [9.2] [10] [11] [12.0] [12.1] Instructions: - - - - - 0.94 0.06 0.92 0.08 - - - 2.00 - - - - - - - - - - add w8, w1, w0 - - - - - 0.03 0.47 0.03 0.47 - - 1.00 - - - - - - - - - - - lsl w9, w8, #1 - - - - - 0.02 0.48 0.02 0.48 - - - - - - - - - - - - - - sub w10, w9, #10 - - - - - 0.47 0.03 0.47 0.03 - - - - - - - - - - - - - - cmp w8, #5 - - - - - 0.03 0.48 0.02 0.47 - - - - - - - - - - - - - - csel w0, w10, w9, gt 0.50 0.50 1.00 - - 0.48 0.01 0.50 0.01 - - - - - - - - - - - - - - ret
这一块内容其实也是之前的详细展示,可以直接意会即可,一般不会细致到每条指令的超标量单元分析。
时间线
来源于
--timeline
:Timeline view: 0123456789 0123456789 012 Index 0123456789 0123456789 0123456789 [0,0] DeeER. . . . . . . . . . . add w8, w1, w0 [0,1] D==eER . . . . . . . . . . lsl w9, w8, #1 [0,2] D===eER . . . . . . . . . . sub w10, w9, #10 [0,3] D==eE-R . . . . . . . . . . cmp w8, #5 [0,4] D====eER . . . . . . . . . . csel w0, w10, w9, gt [0,5] DE-----R . . . . . . . . . . ret [1,0] .D====eeER. . . . . . . . . . add w8, w1, w0 [1,1] .D======eER . . . . . . . . . lsl w9, w8, #1 [1,2] .D=======eER . . . . . . . . . sub w10, w9, #10 [1,3] .D======eE-R . . . . . . . . . cmp w8, #5 [1,4] .D========eER . . . . . . . . . csel w0, w10, w9, gt [1,5] .DE---------R . . . . . . . . . ret [2,0] . D========eeER. . . . . . . . . add w8, w1, w0 [2,1] . D==========eER . . . . . . . . lsl w9, w8, #1 [2,2] . D===========eER . . . . . . . . sub w10, w9, #10 [2,3] . D==========eE-R . . . . . . . . cmp w8, #5 [2,4] . D============eER . . . . . . . . csel w0, w10, w9, gt [2,5] . DE-------------R . . . . . . . . ret [3,0] . D============eeER. . . . . . . . add w8, w1, w0 [3,1] . D==============eER . . . . . . . lsl w9, w8, #1 [3,2] . D===============eER . . . . . . . sub w10, w9, #10 [3,3] . D==============eE-R . . . . . . . cmp w8, #5 [3,4] . D================eER . . . . . . . csel w0, w10, w9, gt [3,5] . DE-----------------R . . . . . . . ret [4,0] . D================eeER. . . . . . . add w8, w1, w0 [4,1] . D==================eER . . . . . . lsl w9, w8, #1 [4,2] . D===================eER . . . . . . sub w10, w9, #10 [4,3] . D==================eE-R . . . . . . cmp w8, #5 [4,4] . D====================eER . . . . . . csel w0, w10, w9, gt [4,5] . DE---------------------R . . . . . . ret [5,0] . D====================eeER. . . . . . add w8, w1, w0 [5,1] . D======================eER . . . . . lsl w9, w8, #1 [5,2] . D=======================eER . . . . . sub w10, w9, #10 [5,3] . D======================eE-R . . . . . cmp w8, #5 [5,4] . D========================eER . . . . . csel w0, w10, w9, gt [5,5] . DE-------------------------R . . . . . ret [6,0] . .D========================eeER. . . . . add w8, w1, w0 [6,1] . .D==========================eER . . . . lsl w9, w8, #1 [6,2] . .D===========================eER . . . . sub w10, w9, #10 [6,3] . .D==========================eE-R . . . . cmp w8, #5 [6,4] . .D============================eER . . . . csel w0, w10, w9, gt [6,5] . .DE-----------------------------R . . . . ret [7,0] . . D============================eeER. . . . add w8, w1, w0 [7,1] . . D==============================eER . . . lsl w9, w8, #1 [7,2] . . D===============================eER . . . sub w10, w9, #10 [7,3] . . D==============================eE-R . . . cmp w8, #5 [7,4] . . D================================eER . . . csel w0, w10, w9, gt [7,5] . . DE---------------------------------R . . . ret [8,0] . . D================================eeER. . . add w8, w1, w0 [8,1] . . D==================================eER . . lsl w9, w8, #1 [8,2] . . D===================================eER . . sub w10, w9, #10 [8,3] . . D==================================eE-R . . cmp w8, #5 [8,4] . . D====================================eER . . csel w0, w10, w9, gt [8,5] . . DE-------------------------------------R . . ret [9,0] . . D====================================eeER. . add w8, w1, w0 [9,1] . . D======================================eER . lsl w9, w8, #1 [9,2] . . D=======================================eER. sub w10, w9, #10 [9,3] . . D======================================eE-R. cmp w8, #5 [9,4] . . D========================================eER csel w0, w10, w9, gt [9,5] . . DE-----------------------------------------R ret 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 19.0 0.1 0.0 add w8, w1, w0 1. 10 21.0 0.0 0.0 lsl w9, w8, #1 2. 10 22.0 0.0 0.0 sub w10, w9, #10 3. 10 21.0 0.0 1.0 cmp w8, #5 4. 10 23.0 0.0 0.0 csel w0, w10, w9, gt 5. 10 1.0 1.0 23.0 ret 10 17.8 0.2 4.0 <total>
这一块是最后一个重点,是模拟执行的流水时间线
D
: 表示分发指令(dispatch)。=
: 表示等待执行。e
: 表示执行指令(execute)。E
: 表示执行完成。-
: 表示等待退役。R
: 表示退役(retire)。参考文献