我正试图使用std::chrono::steady_lock在Raspberry Pi 4上对一段DSP代码进行基准测试,但我得到的结果很奇怪。因为GNU评测工具在Raspberry Pi上不起作用,我一直在使用基准测试来评估代码优化,所以这是一件大事。
当同一测试在同一程序执行中多次运行时,是什么导致性能在基准程序执行之间相差10%,同时保持一致+/-1%?
约6秒基准测试的结果相差约10%。但奇怪的是,对于基准测试的特定执行,差异似乎是粘性的。每次运行程序时,我都会连续运行基准测试三次,得到大致相同的结果+/-1%。但是,当我重新运行该程序时,三个基准测试的结果与上一次运行的结果相差+/-10%,但新运行的三个结果都是+/-1%。
例如:
Run 1:
9:21:37. Performance: 0.0912333 x realtime
9:21:42. Performance: 0.0910667 x realtime
9:21:47. Performance: 0.0910667 x realtime
Run 2:
9:20:15. Performance: 0.106667 x realtime
9:20:21. Performance: 0.1062 x realtime
9:20:28. Performance: 0.106117 x realtime
每次跑步的结果大致在这两个极端之间随机变化。但这里的特殊之处在于,每次运行程序时执行的三个测试之间的结果一致性为+/-1%。
我是一个经验丰富的程序员,所以我知道基准测试会有所不同。但是,对于我正在尝试做的事情来说,大约10%的方差是不可行的。我也无法想出一个合理的理论来解释为什么方差会随着调用而变化。
测试中的代码是一种机器学习算法(LSTM->Dense),使用手工优化的霓虹灯内部函数来生成实时音频。大部分执行(~90%)是使用手工优化的氖气内部函数的矩阵和矢量运算。数据占用空间约为13kb(可轻松放入一级缓存)。代码占用空间未知,但可能不适合L1 i-cache。大多数代码管道都很漂亮,所以代码运行时可能会受到L1缓存带宽限制。到目前为止,优化已经使实时性从0.18倍提高到0.093倍。我认为可能还有15%的改进,但时间上的不准确在这一点上造成了阻碍。测试中的代码执行三次,实时性约为0.3倍,因此进一步的优化实际上是关键的。
已检查的内容:
不是近地天体对齐问题。所有矩阵、矩阵行和向量都是16字节对齐的(在调试编译中使用断言进行检查)。
不是CPU频率问题。CPU缩放调节器已设置为
performance
,并且所有CPU都以1.8Ghz运行我认为这与进程之间的缓存竞争无关。HTOP表示,当通过VNC连接时,空闲时CPU使用率约为6%,当通过ssh连接时,CPU使用率为0.3%(wifi供应商)。当通过SSH连接时,模式不会发生显著变化。
我不认为它会随着代码运行在哪个CPU核心上而变化——尽管我只能使用HTOP来确定代码在特定运行中运行在哪个核心上,这并不是完全确定的。测试运行似乎偶尔会转移到不同的CPU核心,但在大多数情况下,它们似乎在每次执行运行的3个测试期间都在一个随机选择的核心上运行。
我不认为这是热节流。CPU的温度只有47摄氏度。我认为复盆子PI 4s在温度达到80摄氏度之前不会加热。
矢量运算依赖于GCC编译器的自动矢量化,它已经用restrict声明进行了适当的注释,并被验证为产生了最佳的neon矢量化(具有比我用neon内部函数产生的更好的指令调度)。
不是计时器解决问题。对
std::chrono::steady_clock::now()
的连续调用产生37到56ns之间的增量。时钟的选择没有问题。steady_clock、system_clock和high_resolution_clock都表现出相同的行为。
验证的cpu频率:
$ cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
performance
performance
performance
performance
$ cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_cur_freq
1800000
1800000
1800000
1800000
我不知道你可能能帮助的事情:
如何在Raspberry Pi上实现std::chrono::steady_clock。它是基于CPU时钟计数器的吗?如有任何细节,不胜感激。
热节流是否反映在/sys/devices/system/cpu/cpuf*/cpufreq/scaleing_cur_freq中。我想是的,但我不确定。
我显然错过了一些重要的东西。
技术细节:
- 树莓派4b 8GB
- Linux raspberrrypi 5.15.61-v8+#1579 SMP PREEMPT 2022年8月26日星期五英国夏令时11:16:44 aarch64 GNU/Linux
- gcc版本10.2.1 20210110(Debian 10.2.1-6)
- 测试在catch2测试框架下运行
最终确定了问题的根源。这个问题似乎是对一级缓存内容的非常温和的竞争,可能来自一些后台系统进程。
性能计数器表现出与基准测试相同的奇怪行为:每次启动测试程序时,连续3次运行显示基准测试结果的差异约为1%;但不同发射的结果相差约10%。
奇怪的是,测试运行之间的性能差异是一致的,并持续几秒钟。但是,考虑到一级缓存的干扰有多小,很难猜测一百多个正在运行的系统进程中有什么干扰了基准测试,以及为什么会出现这种相当不幸的模式,尤其是因为它们可能以任何调度器优先级运行。
性能计数器的测量结果说明了这个问题:对于具有2995条指令的函数,每次迭代平均有约30次额外的一级数据缓存未命中,这导致了基准测试结果的10%差异。惊人地
我无法猜测哪种系统进程会以在18秒内保持一致的速率污染L1数据缓存,但在更大的时间尺度上会有所不同。
好消息是:测试中的代码非常接近最优。(一个LSTM单元,具有两个实质性的乘法,以及大量的矢量化ArcTan和Sigmoid函数调用),它能够使用超过75%的可用缓存带宽,并且每个时钟周期发出几乎两条指令。呜呜!
测试数据
测试代码每次迭代的平均性能计数器测量值。该程序的每次启动都会运行约6秒的基准测试三次。
测试程序运行良好:
CpuClk : 1,694
L1D Access : 1,244
L1D Miss : 6
L1I Miss : 0
Instructions: 2,995
L2 Access : 12
L2 Miss : 0
---
CpuClk : 1,694
L1D Access : 1,244
L1D Miss : 6
L1I Miss : 0
Instructions: 2,995
L2 Access : 12
L2 Miss : 0
---
CpuClk : 1,693
L1D Access : 1,244
L1D Miss : 6
L1I Miss : 0
Instructions: 2,995
L2 Access : 12
L2 Miss : 0
糟糕的运行:
CpuClk : 1,797
L1D Access : 1,244
L1D Miss : 37
L1I Miss : 0
Instructions: 2,995
L2 Access : 78
L2 Miss : 0
---
CpuClk : 1,794
L1D Access : 1,244
L1D Miss : 37
L1I Miss : 0
Instructions: 2,995
L2 Access : 78
L2 Miss : 0
---
CpuClk : 1,797
L1D Access : 1,244
L1D Miss : 37
L1I Miss : 0
Instructions: 2,995
L2 Access : 78
L2 Miss : 0