案例研究:分析四个基准测试的性能指标
在本章中讨论的所有内容综合起来,我们运行了来自不同领域的四个基准测试,并计算了它们的性能指标。首先,让我们介绍这些基准测试。
- Blender 3.4 - 一个开源的3D创建和建模软件项目。这个测试是使用Blender的Cycles性能进行的,使用了BMW27混合文件。使用了所有的硬件线程。URL: https://download.blender.org/release。命令行:
./blender -b bmw27_cpu.blend -noaudio --enable-autoexec -o output.test -x 1 -F JPEG -f 1
。 - Stockfish 15 - 一个先进的开源国际象棋引擎。这个测试是一个内置的stockfish基准测试。只使用了一个硬件线程。URL: https://stockfishchess.org。命令行:
./stockfish bench 128 1 24 default depth
。 - Clang 15 自我构建 - 这个测试使用clang 15从源代码构建clang 15编译器。使用了所有的硬件线程。URL: https://www.llvm.org。命令行:
ninja -j16 clang
。 - CloverLeaf 2018 - 一个拉格朗日-欧拉流体动力学基准测试。使用了所有的硬件线程。这个测试使用了clover_bm.in输入文件(问题5)。URL: http://uk-mac.github.io/CloverLeaf。命令行:
./clover_leaf
。
为了进行这个练习,我们在具有以下特征的机器上运行了所有四个基准测试:
- 12代 Alderlake Intel(R) Core(TM) i7-1260P CPU @ 2.10GHz (4.70GHz Turbo),4P+8E 核心,18MB L3缓存
- 16 GB DDR4 @ 2400 MT/s内存
- 256GB NVMe PCIe M.2 SSD
- 64位 Ubuntu 22.04.1 LTS (Jammy Jellyfish)
为了收集性能指标,我们使用了toplev.py
脚本,它是pmu-tools1的一部分,由Andi Kleen编写:
$ ~/workspace/pmu-tools/toplev.py -m --global --no-desc -v -- <app with args>
表[@tbl:perf_metrics_case_study]提供了我们四个基准测试的性能指标的并排比较。通过查看这些指标,我们可以了解这些工作负载的性质。在收集性能配置文件并深入研究这些应用程序的代码之前,我们可以对这些基准测试做出一些假设。
Blender。工作在P核心和E核心之间相当均匀分配,两种核心类型的IPC都相当不错。每千条指令的缓存未命中次数相当低(见
L*MPKI
)。分支误预测对性能有所影响:分支误预测比例
指标为2%
;我们每610
条指令就有一个误预测(见IpMispredict
指标),这个数值并不糟糕,但也不完美。TLB并不是瓶颈,因为我们在STLB中很少发生未命中。我们忽略加载未命中延迟
指标,因为缓存未命中的数量非常低。ILP相当高。Goldencove是一个6宽度的体系结构;ILP为3.67
意味着算法几乎每个周期利用了核心资源的2/3
。内存带宽需求很低,只有1.58 GB/s,远低于该机器的理论最大值。从Ip*
指标来看,我们可以得知Blender是一个浮点算法(见IpFLOP
指标),其中有很大一部分是向量化的浮点运算(见IpArith AVX128
)。但是,算法的某些部分也是非向量化的标量浮点单精度指令(IpArith Scal SP
)。另外,请注意每90条指令就会有一个明确的软件内存预取(IpSWPF
);我们期望在Blender的源代码中看到这些提示。结论:Blender的性能受到FP计算的限制,偶尔会出现分支误预测。Stockfish。我们只使用了一个硬件线程运行它,因此E核心上没有任何工作,这是预期的。L1缓存未命中的数量相对较高,但大部分都包含在L2和L3缓存中。分支误预测比例很高;我们每
215
条指令就会付出一次误预测的代价。我们可以估计,我们每215(指令)/ 1.80(IPC)= 120
个周期就会发生一次误预测,这是非常频繁的。与Blender的推理类似,我们可以说TLB和DRAM带宽对Stockfish不构成问题。进一步分析,我们发现工作负载中几乎没有FP操作。结论:Stockfish是一个整数计算工作负载,受分支误预测的影响很大。Clang 15 自我构建。C++代码编译是一项性能特性非常平坦的任务,即没有大的热点。通常,您会发现运行时间归因于许多不同的函数。我们首先注意到的是,P核心比E核心多做了68%的工作,并且IPC要好42%。但是P核心和E核心的IPC都很低。乍一看,
L*MPKI
指标看起来并不令人担忧;然而,结合加载未命中实际延迟(LdMissLat
,以核心时钟表示),我们可以看到缓存未命中的平均成本相当高(~77个周期)。现在,当我们查看*STLB_MPKI
指标时,我们注意到与我们测试的任何其他基准测试都存在实质性差异。这是由于Clang编译器(以及其他编译器)的另一个方面:二进制文件的大小相对较大(超过100 MB)。代码不断跳转到远处的位置,导致TLB子系统的压力很大。正如您所看到的,该问题存在于指令(请参阅Code stlb MPKI
)和数据(请参阅Ld stlb MPKI
)之间。让我们继续进行分析。DRAM带宽使用率高于前两个基准测试,但仍然没有达到我们平台的最大内存带宽的一半(约为25 GB/s)。我们关注的另一个问题是每次调用的指令数量非常少(IpCall
):每个函数调用只有约41条指令。不幸的是,这是编译代码库的本质:它有数千个小函数。编译器需要更积极地内联所有这些函数和包装器。然而,我们怀疑与进行函数调用相关的性能开销仍然是Clang编译器的一个问题。此外,人们可以注意到高ipBranch
和IpMispredict
指标。对于Clang编译,每五条指令中就有一条分支,大约每35条分支中就有一条误预测。几乎没有FP或向量指令,但这并不奇怪。结论:Clang具有庞大的代码库,平坦的性能配置文件,许多小函数和“分支”代码;性能受到数据缓存和TLB未命中以及分支误预测的影响。
- CloverLeaf。与之前一样,我们从分析指令和核心周期开始。P核心和E核心完成的工作量大致相同,但P核心需要更长的时间来完成这项工作,导致P核心上的一个逻辑线程的IPC比一个物理E核心上的IPC低。我们对此还没有一个很好的解释。
L*MPKI
指标很高,特别是每千条指令的L3未命中次数。加载未命中延迟(LdMissLat
)超出了图表范围,表明平均缓存未命中的价格非常高。接下来,我们看一下DRAM带宽使用
指标,发现内存带宽完全饱和了。这就是问题所在:系统中的所有核心共享同一个内存总线,因此它们竞争访问主存,有效地阻塞了执行。CPU缺乏它们需要的数据。进一步说,我们可以看到CloverLeaf几乎没有受到分支误预测或函数调用开销的影响。指令混合主要由FP双精度标量操作主导,代码的某些部分被向量化。结论:多线程CloverLeaf受到内存带宽的限制。
指标名称 | 核心类型 | Blender | Stockfish | Clang15-selfbuild | CloverLeaf |
---|---|---|---|---|---|
指令数 | P 核 | 6.02E+12 | 6.59E+11 | 2.40E+13 | 1.06E+12 |
核周期数 | P 核 | 4.31E+12 | 3.65E+11 | 3.78E+13 | 5.25E+12 |
IPC | P 核 | 1.40 | 1.80 | 0.64 | 0.20 |
CPI | P 核 | 0.72 | 0.55 | 1.57 | 4.96 |
指令数 | E 核 | 4.97E+12 | 0 | 1.43E+13 | 1.11E+12 |
核周期数 | E 核 | 3.73E+12 | 0 | 3.19E+13 | 4.28E+12 |
IPC | E 核 | 1.33 | 0 | 0.45 | 0.26 |
CPI | E 核 | 0.75 | 0 | 2.23 | 3.85 |
L1 失效 MPKI | P 核 | 3.88 | 21.38 | 6.01 | 13.44 |
L2 失效 MPKI | P 核 | 0.15 | 1.67 | 1.09 | 3.58 |
L3 失效 MPKI | P 核 | 0.04 | 0.14 | 0.56 | 3.43 |
Branch 失判率 | E 核 | 0.02 | 0.08 | 0.03 | 0.01 |
代码 TLB MPKI | P 核 | 0 | 0.01 | 0.35 | 0.01 |
加载 TLB MPKI | P 核 | 0.08 | 0.04 | 0.51 | 0.03 |
存储 TLB MPKI | P 核 | 0 | 0.01 | 0.06 | 0.1 |
加载 Miss 延迟 (周期) | P 核 | 12.92 | 10.37 | 76.7 | 253.89 |
指令级并行性 (ILP) | P 核 | 3.67 | 3.65 | 2.93 | 2.53 |
内存级并行性 (MLP) | P 核 | 1.61 | 2.62 | 1.57 | 2.78 |
DRAM 带宽 (GB/s) | 所有 | 1.58 | 1.42 | 10.67 | 24.57 |
函数调用 | 所有 | 176.8 | 153.5 | 40.9 | 2,729 |
指令分支 | 所有 | 9.8 | 10.1 | 5.1 | 18.8 |
加载指令 | 所有 | 3.2 | 3.3 | 3.6 | 2.7 |
存储指令 | 所有 | 7.2 | 7.7 | 5.9 | 22.0 |
指令预测错误 | 所有 | 610.4 | 214.7 | 177.7 | 2,416 |
浮点运算 | 所有 | 1.1 | 1.82E+06 | 286,348 | 1.8 |
算术运算 | 所有 | 4.5 | 7.96E+06 | 268,637 | 2.1 |
IpArith Scal SP | 所有 | 22.9 | 4.07E+09 | 280,583 | 2.60E+09 |
IpArith Scal DP | 所有 | 438.2 | 1.22E+07 | 4.65E+06 | 2.2 |
IpArith AVX128 | 所有 | 6.9 | 0.0 | 1.09E+10 | 1.62E+09 |
IpArith AVX256 | 所有 | 30.3 | 0.0 | 0.0 | 39.6 |
IpSWPF | 所有 | 90.2 | 2,565 | 105,933 | 172,348 |
表:四个基准测试的性能指标表
正如您从这项研究中看到的,仅仅通过查看指标就可以了解很多关于程序行为的信息。它回答了“是什么?”的问题,但没有告诉你“为什么?”。为此,您需要收集性能配置文件,我们将在以后的章节中介绍。本书的第二部分将讨论如何减轻我们分析的四个基准测试中可能出现的性能问题。
请记住,表 @tbl:perf_metrics_case_study 中的性能指标摘要只告诉您程序的平均行为。例如,我们可能看到 CloverLeaf 的 IPC 为 0.2,而实际上它可能永远不会以这样的 IPC 运行,相反它可能有两个持续时间相同的阶段,一个以 IPC 0.1 运行,另一个以 IPC 0.3 运行。性能工具通过为每个指标报告统计数据和平均值来解决这个问题。通常,最小值、最大值、第 95 个百分位数和方差 (stdev/avg) 足以了解分布。此外,一些工具允许绘制数据,因此您可以看到特定指标的值在程序运行期间如何变化。例如,图 @fig:CloverMetricCharts 显示了 CloverLeaf 基准测试中 IPC、L*MPKI、DRAM BW 和平均频率的动态变化。“pmu-tools” 软件包可以在您添加 --xlsx
和 --xchart
选项后自动生成这些图表。
$ ~/workspace/pmu-tools/toplev.py -m --global --no-desc -v --xlsx workload.xlsx –xchart -- ./clover_leaf
尽管与摘要中报告的值偏差不大,但我们可以看到工作负载并不总是稳定的。在查看 IPC 图表后,我们可以假设工作负载中没有不同的阶段,变化是由性能事件的多路复用引起的(在 [@sec:counting] 中讨论)。然而,这只是一个需要证实或否定的假设。可能的方法是通过以更高粒度(在本例中为 10 秒)运行收集来收集更多数据点并研究源代码。仅根据数字得出结论要小心;始终获取第二个数据源来确认您的假设。
总之,查看性能指标有助于构建关于程序中发生了什么和没有发生什么的正确思维模型。深入分析,这些数据将对您大有裨益。
1. pmu-tools - https://github.com/andikleen/pmu-tools ↩