绩效评估的惯用方式?

2024-04-29

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

程序不断运行主循环:

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

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

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

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

这会带来巨大的性能开销。

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

打印日志就够了吗?


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

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

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

许多语言都有基准测试包,可以帮助您编写单个函数的微基准测试。使用它们。例如对于 Java,JMH 确保在执行定时运行之前,JIT 以及所有这些爵士乐对被测函数进行预热和充分优化。并在指定的时间间隔内运行它,计算它完成的迭代次数。看如何用 Java 编写正确的微基准测试? https://stackoverflow.com/questions/504103/how-do-i-write-a-correct-micro-benchmark-in-java为此以及更多。


谨防常见的微基准陷阱

  • 未能预热代码/数据缓存和其他内容:在接触新内存的定时区域内出现页面错误,或代码/数据缓存未命中,这不是正常操作的一部分。 (注意到这种效果的示例:性能:memset https://stackoverflow.com/questions/23723215/performance-memset;或一个例子基于这个错误得出错误的结论 https://stackoverflow.com/questions/57125253/why-is-iterating-though-stdvector-faster-than-iterating-though-stdarray)

  • 如果您在不写入的情况下进行读取,则从未写入的内存(从内核新鲜获得)会将其所有页面写入时复制映射到同一个系统范围的零物理页面(4K 或 2M)on Linux https://stackoverflow.com/questions/57125253/why-is-iterating-though-stdvector-faster-than-iterating-though-stdarray/57130924#57130924。因此,您可以获得缓存命中但 TLB 未命中。例如大量拨款来自new / calloc / malloc,或静态存储中的零初始化数组.bss。使用非零初始值设定项或 memset。

  • 未能给 CPU 时间加速到最大睿频:现代 CPU 会降低至空闲速度以节省电量,仅在几毫秒后才开始加速。 (或更长,具体取决于操作系统/硬件)。

    相关:在现代 x86 上,RDTSC 计算参考周期,而不是核心时钟周期 https://stackoverflow.com/questions/13772567/how-to-get-the-cpu-cycle-count-in-x86-64-from-c/51907627#51907627,因此它会受到与挂钟时间相同的 CPU 频率变化影响。

  • 大多数整数和 FP 算术汇编指令 (除了除法和平方根 https://stackoverflow.com/questions/4125033/floating-point-division-vs-floating-point-multiplication/45899202#45899202已经比其他慢)的性能(延迟和吞吐量)不依赖于实际数据。除了次正规浮点数外being very slow https://stackoverflow.com/questions/60969892/performance-penalty-denormalized-numbers-versus-branch-mis-predictions,并且在某些情况下(例如旧版 x87,但不包括 SSE2 https://stackoverflow.com/questions/31875464/huge-performance-difference-26x-faster-when-compiling-for-32-and-64-bits/31879376#31879376)同时生成 NaN 或 Inf 可能会很慢。

  • 在乱序执行的现代 CPU 上,有些事情太短暂,无法真正有意义地度过 https://stackoverflow.com/questions/54621381/rdtscp-in-nasm-always-returns-the-same-value, 也可以看看this https://stackoverflow.com/questions/51607391/what-considerations-go-into-predicting-latency-for-operations-on-modern-supersca. 一小段汇编语言(例如由编译器为一个函数生成)的性能不能用单个数字来表征,即使它不分支或访问内存(因此不会出现错误预测或缓存未命中)。它从输入到输出有延迟,但如果使用独立输入重复运行,则不同的吞吐量会更高。例如一个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 的常数幂,编译器将只使用移位。源代码中的相同运算符可以编译为非常不同的指令,具体取决于周围的代码。

  • You 需要在启用优化的情况下进行编译 https://stackoverflow.com/questions/32000917/c-loop-optimization-help-for-final-assignment-with-compiler-optimization-disabl/32001196#32001196,但您还需要阻止编译器优化工作,或将其提升出循环。确保使用结果(例如打印它或将其存储到volatile)所以编译器必须生成它。对于数组,volatile double sink = output[argc];是一个有用的技巧:编译器不知道argc所以它必须生成整个数组,但您不需要读取整个数组,甚至不需要调用 RNG 函数。 (除非编译器积极地转换为仅计算由argc,但这在实践中往往不是问题。)

    对于输入,使用随机数或argc或其他东西而不是编译时常量,这样您的编译器就无法对实际用例中不是常量的东西进行常量传播。在 C 中,有时可以使用内联汇编或volatile为此,例如东西这个问题问的是 https://stackoverflow.com/questions/33975479/escape-and-clobber-equivalent-in-msvc。一个很好的基准测试包,例如谷歌基准测试 https://github.com/google/benchmark将包含这方面的功能。

  • 如果函数的实际用例让它内联到调用者中,其中某些输入是恒定的,或者操作可以优化到其他工作中,那么单独对其进行基准测试并不是很有用。

  • 当您重复运行它们时,对许多特殊情况进行特殊处理的大型复杂函数可以在微基准测试中看起来很快,尤其是使用same每次都输入。在现实生活中的用例中,分支预测通常不会为该输入的函数做好准备。此外,大规模展开的循环在微基准测试中看起来不错,但在现实生活中,它会因其庞大的指令缓存占用空间而减慢其他一切,从而导致其他代码被驱逐。

与最后一点相关:如果函数的实际用例包含大量小输入,则不要仅针对大量输入进行调整。例如Amemcpy对于大量输入来说非常有用,但需要很长时间才能弄清楚对于小输入使用哪种策略可能不太好。这是一个权衡;确保它对于大输入来说足够好(对于“足够”的适当定义),但对于小输入也保持较低的开销。

石蕊测试:

  • 如果您要对一个程序中的两个函数进行基准测试:如果颠倒测试顺序会改变结果,则您的基准测试不公平。例如函数 A 可能看起来很慢,因为您首先测试它,没有充分的预热。例子:为什么 std::vector 比数组慢? https://stackoverflow.com/questions/60293633/why-is-stdvector-slower-than-an-array(事实并非如此,无论哪个循环先运行都必须为所有页面错误和缓存未命中付出代价;第二个循环只是通过填充相同的内存来进行缩放。)

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

  • 改变其他测试参数作为健全性检查。


对于 C / C++,另请参阅 简单的 for() 循环基准测试与任何循环绑定花费相同的时间 https://stackoverflow.com/questions/50924929/simple-for-loop-benchmark-takes-the-same-time-with-any-loop-bound/50934895#50934895我在其中详细介绍了微基准测试和使用volatile or asm阻止重要工作使用 gcc/clang 进行优化。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

绩效评估的惯用方式? 的相关文章

随机推荐