我正在寻找相关的性能指标来基准测试和优化我的 C/C++ 代码。例如,虚拟内存使用情况是一个简单但有效的指标,但我知道有些更专业,有助于优化特定域:缓存命中/未命中、上下文切换等。
我相信这里有一个很好的地方,可以列出绩效指标,它们衡量什么以及如何衡量它们,以帮助想要开始优化程序的人知道从哪里开始。
时间是最相关的指标。
这就是为什么大多数分析器默认为测量/采样时间或内核时钟周期的原因。 了解代码花费时间的位置是寻找加速的重要第一步。首先找出什么是慢的,然后找出它为什么慢。
您可以寻找 2 种根本不同的加速,时间将帮助您找到这两种加速。
-
算法改进:首先找到减少工作量的方法。 这通常是最重要的类型,也是Mike Dunlavey的答案所关注的。 你绝对不应该忽视这一点。 缓存重新计算速度慢的结果是非常值得的,特别是如果它足够慢,以至于从 DRAM 加载仍然更快。
使用可以在实际CPU上更有效地解决问题的数据结构/算法介于这两种加速之间。 (例如,链表在实践中通常比数组慢,因为指针追踪延迟是一个瓶颈,除非你最终过于频繁地复制大型数组......
-
更有效地应用蛮力,在更少的周期内完成相同的工作。(和/或对程序的其余部分更友好,缓存占用空间更小和/或占用分支预测器空间的分支更少,或者其他什么。
通常涉及更改数据布局以使其对缓存更友好,和/或使用 SIMD 手动矢量化。 或者以更聪明的方式这样做。 或者编写一个比一般情况函数更快地处理常见特殊情况的函数。 甚至手动控制编译器为您的 C 源代码制作更好的 asm。
考虑对现代 x86-64 上的
float
数组求和:从延迟限制标量添加到具有多个累加器的 AVX SIMD 可以为您提供 8(每个矢量的元素)* 8(Skylake 上的延迟/吞吐量)= 64 倍的加速对于中型阵列(仍在单个内核/线程上),在理论上最好的情况下,您不会遇到另一个瓶颈(例如,如果数据在 L1d 缓存中不热,则为内存带宽)。 Skylakevaddps
/vaddss
具有 4 个周期延迟,每个时钟 2 个 = 0.5c 倒数吞吐量。 (https://agner.org/optimize/)。 为什么 mulss 在 Haswell 上只需要 3 个周期,与 Agner 的指令表不同?有关多个累加器以隐藏 FP 延迟的更多信息。 但这仍然会输给将总数存储在某处,甚至可能在更改元素时用增量更新总数。 (不过,与整数不同,FP 舍入误差可以以这种方式累积。
如果您没有看到明显的算法改进,或者想在进行更改之前了解更多信息,请检查 CPU 是否在任何事情上停滞不前,或者它是否在效率上咀嚼编译器所做的所有工作。
每时钟指令数 (IPC)告诉您 CPU 是否接近其最大指令吞吐量。 (或者更准确地说,在 x86 上按时钟发出的融合域 uop,因为例如,一条rep movsb
指令是一个很大的内存,并解码为许多 uop。 CMP/JCC 从 2 条指令熔断到 1 uop,增加了 IPC,但流水线宽度仍然是固定的。
每条指令完成的工作也是一个因素,但不是你可以用分析器来衡量的:如果你有专业知识,请查看编译器生成的ASM,看看是否可以用更少的指令完成相同的工作。 如果编译器没有自动矢量化,或者效率低下,则根据问题的不同,通过使用 SIMD 内部函数手动矢量化,每条指令可以完成更多的工作。 或者通过手动控制编译器以更好地发射 asm,通过调整 C 源以 asm 自然的方式计算事物。 例如,在某个位置或更低位置计算设置位的有效方法是什么? 另请参阅C++比手写汇编更快地测试 Collatz 猜想的代码 - 为什么?
如果您发现 IPC 较低,请通过考虑缓存未命中或分支未命中或长依赖链(通常是前端或内存上没有瓶颈时 IPC 低的原因)等可能性来找出原因。
或者您可能会发现它已经接近最佳地应用 CPU 的可用暴力(不太可能,但对于某些问题可能)。 在这种情况下,你唯一的希望是算法改进以减少工作。
(CPU 频率不是固定的,但内核时钟周期是一个很好的代理。 如果您的程序不花时间等待 I/O,那么测量内核时钟周期可能更有用。
多线程程序的大部分串行部分可能很难检测到;当其他线程被阻塞时,大多数工具都没有一种简单的方法来使用循环来查找线程。
不过,在函数中花费的时间并不是唯一的指标。函数可以通过接触大量内存来使程序的其余部分变慢,从而导致从缓存中逐出其他有用的数据。 所以这种效果是可能的。 或者,在某处有很多分支可能会占用CPU的一些分支预测能力,从而导致其他地方的分支错过更多。
但请注意,简单地查找 CPU 花费大量时间执行的位置并不是最有用的,在包含热点的函数可以有多个调用方的大型代码库中。 例如,在memcpy上花费大量时间并不意味着您需要加快memcpy的速度,而是意味着您需要找到哪个调用者经常调用memcpy。 依此类推,备份调用树。
使用可以记录堆栈快照的探查器,或者只需在调试器中按 control-C 并查看调用堆栈几次。 如果某个函数通常出现在调用堆栈中,则它正在进行昂贵的调用。
相关:linux perf:如何解释和查找热点,尤其是Mike Dunlavey的回答表明了这一点。
避免做工作的算法改进通常比更有效地做同样的工作更有价值。
但是,如果您发现某些工作的 IPC 非常低,您还没有弄清楚如何避免,那么请务必查看重新排列数据结构以获得更好的缓存,或避免分支错误预测。
或者,如果高 IPC 仍然需要很长时间,手动矢量化循环会有所帮助,每条指令执行 4 倍或更多的工作。
@PeterCordes答案总是好的。我只能添加我自己的观点,来自大约 40 年的优化代码:
如果有时间需要节省(确实有),那么这些时间就会花在做一些不必要的事情上,如果你知道它是什么,你可以摆脱它。
那是什么呢?既然你不知道它是什么,你也不知道它需要多少时间,但它确实需要时间。花费的时间越多,就越值得找到,也就越容易找到它。假设它需要 30% 的时间。这意味着随机时间快照有 30% 的机会向您展示它是什么。
我使用调试器和"暂停"函数对调用堆栈进行 5-10 个随机快照。
如果我看到它在多个快照上做某事,并且该事情可以更快地完成或根本不完成,那么我保证会大幅加速。 然后可以重复该过程以找到更多的加速,直到我达到收益递减。
这种方法的重要之处在于 -没有"瓶颈"可以躲避它。这使它与分析器区分开来,因为它们总结了加速可以隐藏它们。