C/C++ 最相关的性能指标

Most relevant performance indicators for C/C++

我正在寻找相关的性能指标来对我的 C/C++ 代码进行基准测试和优化。例如,虚拟内存使用是一个简单但有效的指标,但我知道有些更专业,有助于优化特定领域:缓存 hits/misses、上下文切换等。

我相信这里是列出性能指标、衡量的内容以及衡量方式的好地方,以帮助想要开始优化程序的人们知道从哪里开始开始。

时间是最相关的指标。

这就是大多数分析器默认测量/采样时间或核心时钟周期的原因。了解您的代码将时间花在哪里是寻求加速的重要第一步。 首先找出什么慢,然后找出为什么它慢

您可以寻找 2 种根本不同的加速,时间会帮助您找到它们。

  • 算法改进:首先找到减少工作量的方法。这通常是最重要的一种,也是 Mike Dunlavey 的回答所关注的。你绝对应该 而不是 忽略它。缓存一个重新计算缓慢的结果是非常值得的,特别是如果它足够慢以至于从 DRAM 加载仍然更快。

    使用可以更有效地解决真实 CPU 问题的数据结构/算法介于这两种加速之间。 (例如,链表在实践中通常比数组慢,因为指针追踪延迟是一个瓶颈,除非你最终过于频繁地复制大型数组......)

  • 更有效地使用蛮力以更少的周期完成相同的工作。 (And/or 对程序的其余部分更友好,缓存占用空间更小 and/or 分支预测器中占用 space 的分支更少,或其他。)

    通常涉及更改数据布局以更加缓存友好,and/or 使用 SIMD 手动矢量化。或者以更聪明的方式这样做。或者编写一个函数来处理常见的特殊情况,比一般情况下的函数更快。或者甚至可以让编译器为您的 C 源代码制作更好的 asm。

    考虑在现代 x86-64 上对 float 的数组求和:从延迟绑定标量加法到具有多个累加器的 AVX SIMD 可以使您加速 8(每个向量的元素)* 8(延迟/ Skylake 上的吞吐量)= 64x 对于中型阵列(仍然在单个 core/thread 上),在理论上最好的情况下你不会 运行 进入另一个瓶颈(比如内存带宽,如果你的数据在 L1d 缓存中不热)。 Skylake vaddps / vaddss 有 4 个周期延迟,每个时钟 2 个 = 0.5c 互惠吞吐量。 (https://agner.org/optimize/). 了解更多关于多个累加器以隐藏 FP 延迟的信息。但这仍然很难与将总数存储在某处相比,甚至可能在更改元素时用增量更新总数。(FP 舍入误差可以累积不过,这与整数不同。)


如果您没有看到明显的算法改进,或者想在进行更改之前了解更多信息,请检查 CPU 是否在拖延任何事情,或者编译器是否在高效地咀嚼所有工作做到了。

每时钟指令数 (IPC) 告诉您 CPU 是否接近其最大指令吞吐量。 (或更准确地说,在 x86 上每个时钟发出融合域微指令,因为例如一个 rep movsb 指令是一个完整的大 memcpy 并解码为许多微指令。并且 cmp/jcc 从 2 条指令融合到 1 微指令, 增加 IPC 但流水线宽度仍然固定。)

每条指令完成的工作也是一个因素,但这不是您可以使用分析器衡量的:如果您有专业知识,请查看编译器生成的 asm 以看看是否可以用更少的指令完成同样的工作。如果编译器没有自动矢量化,或者这样做效率低下,您可以通过使用 SIMD 内在函数手动矢量化来为每条指令完成更多工作,具体取决于问题。或者通过调整您的 C 源代码以对 asm 来说自然的方式计算事物,让编译器发出更好的 asm。例如. And see also


如果您发现 IPC 较低,请通过考虑诸如缓存未命中或分支未命中或长依赖链(通常是 IPC 未出现瓶颈时的低 IPC 原因)等可能性来找出原因前端或内存)。

或者您可能会发现它已经接近最佳地应用 CPU 的可用蛮力(不太可能,但对于某些问题是可能的)。在那种情况下,您唯一的希望就是改进算法以减少工作量。

(CPU 频率不固定,但核心时钟周期是一个很好的代表。如果您的程序不花时间等待 I/O,那么核心时钟周期可能是 更多有用衡量。)

多线程程序的大部分串行部分可能很难检测到;当其他线程被阻塞时,大多数工具都没有一种简单的方法来使用循环查找线程。


在函数中花费的时间并不是 唯一的 指标。 一个函数会占用大量内存,从而使程序的其余部分变慢,从而导致其他有用数据从缓存中被逐出。所以那种效果是可能的。或者在某个地方有很多分支可能会占用 CPU 的一些分支预测容量,导致其他地方有更多分支未命中。


但是请注意,仅仅找到 CPU 花费大量时间执行的位置并不是最有用的, 在包含热点的函数可能具有的大型代码库中多个来电者。例如在 memcpy 中花费大量时间并不意味着您需要加速 memcpy,这意味着您需要找到哪个调用者经常调用 memcpy。依此类推备份调用树。

使用可以记录堆栈快照的分析器,或者在调试器中按下 control-C 并查看调用堆栈几次。如果某个函数通常出现在调用堆栈中,则它正在进行昂贵的调用。

相关:linux perf: how to interpret and find hotspots,尤其是 Mike Dunlavey 的回答说明了这一点。


通过改进算法来避免做任何工作通常比更有效地做同样的工作更有价值。

但是,如果您发现某些工作的 IPC 非常低,您还没有想出如何避免,那么一定要重新安排您的数据结构以获得更好的缓存,或避免分支预测错误。

或者如果高 IPC 仍然需要很长时间,手动矢量化循环会有所帮助,每条指令执行 4 倍或更多的工作。

@PeterCordes 的回答总是很好。我只能添加我自己的观点,来自大约 40 年的优化代码:

如果有时间可以节省(确实有),那么这些时间就会花在做一些不必要的事情上,如果你知道它是什么,你就可以摆脱它。

那是什么?因为你不知道它是什么,所以你也不知道它需要多少时间,但是它确实需要时间。花的时间越多,越值得找,也越容易找到。假设它占用了 30% 的时间。这意味着随机时间快照有 30% 的机会向您展示它是什么。

我使用调试器和 "pause" 函数拍摄了 5-10 个调用堆栈的随机快照。

如果我看到它在不止一个快照上做某事,并且那件事可以更快地完成或根本不完成,我保证有很大的加速。 然后可以重复该过程以找到更多加速,直到我达到递减 returns.

此方法的重要之处在于 - "bottleneck" 无法躲避它。这使它有别于分析器,因为它们总结了加速可以隐藏它们。