为什么这个延迟循环在几次没有休眠的迭代后开始运行得更快?

考虑一下:

#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。

当取消 usleep注释时,所有运行的延迟循环需要0.4 ms,从不加速。为什么?

代码是用 g++ -O0编译的(没有优化) ,所以延迟循环没有被优化掉。它运行在 Intel (R) Core (TM) I3-3220 CPU@3.30 GHz 上,配有3.13.0-32-general Ubuntu 14.04.1 LTS (Trusty Tahr)。

7827 次浏览

usleep的调用可能会导致上下文切换,也可能不会导致上下文切换。如果是这样,那就比不是这样要花更长的时间。

经过26次迭代之后,Linux 将 CPU 的时钟速度提升到最高,因为您的进程连续几次使用完整的 时间片

如果你用性能计数器而不是挂钟时间检查,你会看到每个延迟循环的核心时钟周期保持不变,证实这只是 DVFS的影响(所有现代 CPU 大部分时间都使用 DVFS以更节能的频率和电压运行)。

如果您在支持 新的电源管理模式(硬件完全控制时钟速度)的内核的 天湖上进行测试,那么升级速度会快得多。

如果您让它在 带涡轮的英特尔 CPU上运行一段时间,您可能会看到每次迭代的时间再次轻微增加,一旦热限制要求时钟速度降低到最大持续频率。(请参阅 为什么我的 CPU 不能在高性能计算中保持最佳性能了解更多关于 Turbo 让 CPU 运行速度超过其承受大功率工作负载的能力的信息。)


引入 usleep 可以防止 Linux 的 CPU 频率调节器提高时钟速度,因为即使在最低频率下进程也不会产生100% 的负载。(也就是说,内核的启发式决定了 CPU 的运行速度足够快,可以承受运行在上面的工作负载。)



对其他理论的评论 :

Re: David 的理论是,从 usleep切换上下文可能会污染缓存: 这通常是个不错的主意,但它无助于解释这段代码。

缓存/TLB 污染对于这个实验根本不重要。除了堆栈的末端,计时窗口中基本上没有什么东西触及到内存。大部分时间花费在一个小循环(一行指令缓存)中,这个循环只接触一个 int堆栈内存。在 usleep期间,任何潜在的缓存污染都只是这段代码的一小部分时间(真正的代码会有所不同) !

X86的更多细节:

clock()本身的调用可能缓存未命中,但代码获取缓存未命中会延迟开始时间的测量,而不是测量的内容的一部分。对 clock()的第二次调用几乎不会延迟,因为它在缓存中仍然是热的。

run函数可能位于与 main不同的缓存行中(因为 gcc 将 main标记为“ cold”,所以它得到的优化较少,并与其他冷函数/数据放在一起)。我们可以期待一个或两个 指令-缓存未命中。他们可能仍然在相同的4k 页,但是,所以 main将触发潜在的 TLB 错过之前进入计划的时间区域。

Gcc-O0将把 OP 的代码编译成 类似这样的东西(Godbolt 编译器浏览器): 将循环计数器保存在堆栈的内存中。

空循环将循环计数器保存在堆栈内存中,因此在典型的 Intel x86 CPU上,循环在 OP 的 IvyBridge CPU 上以每6个周期一次迭代的速度运行,这要感谢存储转发延迟,这是 add的一部分,带有一个内存目标(读-修改-写)。100k iterations * 6 cycles/iteration是600k 周期,这主导了最多一对缓存丢失的贡献(每个约200周期的代码获取丢失阻止进一步的指令发出,直到它们被解决)。

乱序执行和存储转发应该主要隐藏访问堆栈时可能出现的缓存丢失(作为 call指令的一部分)。

即使循环计数器保存在寄存器中,100k 周期也是很多的。