首页 > 解决方案 > 绩效评估的惯用方式?

问题描述

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

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

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

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

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

我担心的是,如果我打印每个程序的每个入口和出口的时间间隔,

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

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

打印日志是否足够好?

标签: benchmarkingmicrobenchmark

解决方案


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

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

这个问题太笼统了,不能说更具体的。

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

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

  • 未能预热代码/数据缓存和东西:在定时区域内触摸新内存的页面错误,或代码/数据缓存未命中,这不属于正常操作的一部分。(注意到这种影响的例子:Performance: memset example of a wrong evidence based on this error
  • 未能给 CPU 时间加速到最大涡轮增压:现代 CPU 将时钟降至空闲速度以节省电力,仅在几毫秒后才加速。(或更长,取决于操作系统/硬件)。

相关:在现代 x86 上,RDTSC 计算参考周期,而不是核心时钟周期,因此它受到与挂钟时间相同的 CPU 频率变化影响。

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

显然,CPU 之间的性能可能不同;在大局中,A 版在 Intel 上更快,B 版在 AMD 上更快,但这种情况很容易在小范围内发生。在报告/记录基准数值时,请始终注意您测试的 CPU。

  • 与以上和以下几点相关:例如,您不能对*C 中的运算符进行基准测试。它的一些用例的编译将与其他用例非常不同,例如tmp = foo * i;在循环中通常会变成tmp += foo(强度降低),或者如果乘数是 2 的恒定幂,则编译器将只使用移位。源代码中的相同运算符可以编译成非常不同的指令,具体取决于周围的代码。
  • 需要在启用优化的情况下进行编译,但您还需要阻止编译器优化工作或将其提升出循环。确保您使用结果(例如打印它或将其存储到 a volatile),以便编译器必须生成它。使用随机数或其他东西而不是编译时常量作为输入,这样您的编译器就无法对在您的实际用例中不是常量的东西进行常量传播。在 C 中,您有时可以使用内联 asm 或volatile为此,例如这个问题所问的东西。像Google Benchmark这样的优秀基准测试包将包含此功能。
  • 如果一个函数的真实用例允许它内联到调用者中,其中一些输入是恒定的,或者操作可以优化到其他工作中,那么单独对其进行基准测试并不是很有用。
  • 当您重复运行它们时,对许多特殊情况进行特殊处理的大型复杂函数在微基准测试中看起来很快,尤其是每次使用相同的输入时。在现实生活中的用例中,分支预测通常不会为具有该输入的该功能做好准备。此外,一个大规模展开的循环在微基准测试中看起来不错,但在现实生活中,它的大指令缓存占用空间会导致其他代码被驱逐,从而减慢其他一切。

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

石蕊测试:

  • 如果您在一个程序中对两个函数进行基准测试:如果颠倒测试顺序会改变结果,那么您的基准测试是不公平的。例如,功能 A 可能只是看起来很慢,因为您首先要对其进行测试,而预热不足。示例:为什么 std::vector 比数组慢?(事实并非如此,无论哪个循环首先运行都必须为所有页面错误和缓存未命中支付费用;第二个循环只是通过填充相同的内存进行缩放。)

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

即改变测试参数作为健全性检查。


对于 C / C++,另请参阅 简单的 for() 循环基准测试与任何循环绑定都需要相同的时间,我在其中详细介绍了微基准测试和使用volatileasm停止使用 gcc/clang 优化重要工作。


推荐阅读