绩效评估的惯用方式?

Idiomatic way of performance evaluation?

我正在为我的项目评估网络+渲染工作量。

程序连续运行一个主循环:

while (true) {
   doSomething()
   drawSomething()
   doSomething2()
   sendSomething()
}

主循环每秒运行超过60次。

我想查看性能细分,每个过程需要多少时间。

我担心的是,如果我打印每个过程的每次进入和退出的时间间隔,

这会产生巨大的性能开销。

我很好奇衡量性能的惯用方法是什么。

日志打印是否足够好?

一般:对于重复的短的东西,你可以只对整个重复循环计时。 (但是微基准测试很难;除非您了解这样做的含义,否则很容易扭曲结果;对于非常短的事情,吞吐量和延迟是不同的,因此通过使一次迭代使用或不使用前一次的结果来分别测量两者。还要注意分支预测和缓存可以使某些东西在微基准测试中看起来很快,而如果在较大程序的其他工作之间一次完成一个,实际上成本很高。 例如循环展开和查找表通常看起来不错,因为没有其他任何东西对 I-cache 或 D-cache 造成压力。)

或者,如果您坚持对每个单独的迭代进行计时,请将结果记录在数组中并稍后打印;您不想在循环中调用重量级打印代码。

这个问题太宽泛了,无法说得更具体。

许多语言都有基准测试包,可以帮助您编写单个函数的微基准测试。使用它们。例如对于 Java,JMH 确保被测函数在执行定时 运行s 之前由 JIT 和所有爵士乐预热和完全优化。 运行在指定的时间间隔内计算它完成了多少次迭代。

当心常见的微基准测试陷阱:

  • 无法预热代码/数据缓存和其他内容:在定时区域内因接触新内存而出现页面错误,或者代码/数据缓存未命中,这不是正常操作的一部分。 (注意到此效果的示例:Performance: memset example of a
  • 未能给 CPU 时间来加速到最大涡轮增压:现代 CPUs 时钟降低到怠速以节省电力,仅在几毫秒后才加速。 (或更长取决于 OS / HW)。

相关:在现代 x86 上,RDTSC counts reference cycles, not core clock cycles,因此它受到与挂钟时间相同的 CPU 频率变化影响。

  • 在具有乱序执行的现代 CPU 上,, see also this一小块汇编语言的性能(例如,由编译器为一个函数生成)不能用一个数字来表征,即使它不分支或访问内存(所以没有错误预测或缓存未命中的机会)。它从输入到输出有延迟,但如果 运行 重复使用独立输入更高,则吞吐量不同。例如Skylake CPU 上的 add 指令具有 4 个时钟的吞吐量,但有 1 个周期的延迟。所以 dummy = foo(x) 在循环中比 x = foo(x); 快 4 倍。浮点指令比整数指令有更高的延迟,所以它通常是一个更大的问题。在大多数 CPU 上,内存访问也是流水线式的,因此遍历数组(下一次加载的地址很容易计算)通常比遍历链表快得多(下一次加载的地址在上一次加载之前不可用)完成)。

显然 CPU 之间的性能可能不同;从大的角度来看,通常很少见 A 版在 Intel 上更快,B 版在 AMD 上更快,但这在小规模上很容易发生。在报告/记录基准数据时,请始终注意 CPU 您测试的内容。

  • 与以上和以下几点相关:例如,您不能对 C 中的 * 运算符进行基准测试。它的一些用例将与其他用例编译非常不同,例如循环中的 tmp = foo * i; 通常会变成 tmp += foo(强度降低),或者如果乘数是 2 的常数次方,编译器将只使用移位。源代码中的相同运算符可以编译为非常不同的指令,具体取决于周围的代码。
  • , but you also need to stop the compiler from optimizing away the work, or hoisting it out of a loop. Make sure you use the result (e.g. print it or store it to a volatile) so the compiler has to produce it. Use a random number or something instead of a compile-time constant for an input so your compiler can't do constant-propagation for things that won't be constants in your real use-case. In C you can sometimes use inline asm or volatile for this, e.g. the stuff this question is asking about. A good benchmarking package like Google Benchmark 将包含此功能。
  • 如果一个函数的实际用例允许它内联到某些输入是常量的调用者中,或者操作可以优化到其他工作中,那么单独对其进行基准测试并不是很有用。
  • 当您重复 运行 时,对许多特殊情况进行特殊处理的大型复杂函数可以在微基准测试中看起来很快,尤其是每次输入 相同 时。在现实生活中的用例中,分支预测通常不会为具有该输入的该功能做好准备。此外,大规模展开的循环在微基准测试中看起来不错,但在现实生活中,它会减慢其他一切,因为它的大指令缓存占用空间会导致其他代码被逐出。

与最后一点相关:如果函数的实际用例包含大量小输入,则不要只针对大输入进行调整。例如memcpy 实现非常适合大量输入,但需要很长时间才能确定用于少量输入的策略可能不好。这是一种权衡;确保它对大输入足够好,但也要为小输入保持低开销。

石蕊试纸:

  • 如果您在一个程序中对两个函数进行基准测试:如果颠倒测试顺序会改变结果,那么您的基准测试是不公平的。例如功能 A 可能看起来很慢,因为您首先对其进行测试,预热不足。示例:(不是,第一个循环 运行s 必须为所有页面错误和缓存未命中付出代价;第二个只是通过填充相同的内存来放大。)

  • 增加重复循环的迭代次数应该会线性增加总时间,并且不会影响计算的每次调用时间。如果不是,那么您的测量开销是不可忽略的,或者您的代码已被优化掉(例如,提升到循环之外并且 运行s 仅一次而不是 N 次)。

即改变测试参数作为完整性检查。


对于 C/C++,另请参阅 Simple for() loop benchmark takes the same time with any loop bound,其中我详细介绍了微基准测试并使用 volatileasm使用 gcc/clang.

停止重要工作的优化