考虑:
#include <time.h> #include <unistd.h> #include <iostream> using namespace std; const int times = 1000; const int N = 100000; void run() { for (int j = 0; j < N; j++) { } } int main() { clock_t main_start = clock(); for (int i = 0; i < times; i++) { clock_t start = clock(); run(); cout << "cost: " << (clock() - start) / 1000.0 << " ms." << endl; //usleep(1000); } cout << "total cost: " << (clock() - main_start) / 1000.0 << " ms." << endl; }
这是示例代码。在时序循环的前26次迭代中,该run函数的成本约为0.4 ms,但随后成本降低为0.2 ms。
run
当usleep被注释掉,延迟环需要0.4毫秒所有的运行,从未加快。为什么?
usleep
该代码是使用g++ -O0(无需优化)编译的,因此不会优化延迟循环。它可以在3.30 GHz的Intel®CoreTM i3-3220 CPU 上运行,并具有3.13.0-32通用的Ubuntu 14.04.1 LTS(Trusty Tahr)。
g++ -O0
经过26次迭代后,Linux将CPU提升至最大时钟速度,因为您的进程连续两次使用其全部时间片。
如果您使用性能计数器而不是挂钟时间进行检查,您会发现每个延迟环的核心时钟周期保持恒定,从而确认这只是DVFS的作用(所有现代CPU都使用DVFS以更高的能耗运行- 大部分时间都是有效的频率和电压)。
如果您在支持新电源管理模式(硬件完全控制时钟速度的内核)的Skylake上进行了测试,则加速会更快。
如果您将它在带有Turbo的Intel CPU上运行一段时间,则可能会发现,一旦散热限制要求时钟速度降低到最大持续频率,每次迭代的时间就会再次稍微增加。(有关Turbo的更多信息,请参见为什么我的CPU无法在HPC中保持峰值性能,更多有关Turbo使CPU的运行速度超过其在高功率工作负载下的承受能力的信息。)
引入ausleep可以防止Linux的CPU频率调节器提高时钟速度,因为即使在最低频率下,该过程也不会产生100%的负载。(即,内核的启发式方法决定CPU的运行速度足以满足其上正在运行的工作负载。)
缓存/ TLB污染对于该实验根本不重要 。除了堆栈的末尾,时序窗口内基本上没有其他东西可以接触内存。大部分时间都花在一个很小的循环(1行指令高速缓存)中,该循环仅接触int堆栈存储器之一。usleep对于此代码,任何潜在的高速缓存污染时间仅占该代码时间的一小部分(实际代码将有所不同)!
int
对于x86更详细:
对其clock()自身的调用可能会丢失高速缓存,但是代码获取高速缓存未命中会延迟开始时间的测量,而不是被测量的一部分。clock()几乎永远不会延迟对的第二次调用,因为它在缓存中仍然很热。
clock()
该run函数可能位于与之不同的缓存行中main(因为gcc标记main为“冷”,因此它的优化程度较低,并与其他冷函数/数据一起放置)。我们可以预期会有一两个指令高速缓存未命中。但是,它们可能仍在同一4k页面中,因此main在进入程序的定时区域之前将触发潜在的TLB丢失。
main
gcc -O0会将OP的代码编译为如下代码(Godbolt编译器浏览器):将循环计数器保存在堆栈中的内存中。
空循环将循环计数器保持在堆栈内存中,因此,在典型的Intel x86 CPU上,循环的运行是在OP的IvyBridge CPU上每6个周期执行一次迭代,这要归功于add存储目标的一部分的存储转发延迟(读-modify-write)。 100k iterations * 6 cycles/iteration周期为60万个周期,最多可控制几个高速缓存未命中(每个200个周期用于代码提取未命中,这会阻止进一步的指令发出,直到它们被解决为止)。
add
100k iterations * 6 cycles/iteration
乱序执行和存储转发应在访问堆栈时(作为call指令的一部分)在大多数情况下隐藏潜在的高速缓存未命中。
call
即使将循环计数器保存在寄存器中,也要花费100k个周期。