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

时间:2016-07-11 04:06:30

标签: c++ linux performance benchmarking

考虑:

#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毫秒,从不加速。为什么呢?

代码使用g++ -O0编译(无优化),因此延迟循环不会被优化掉。它运行在Intel(R)Core({3}} CPU @ 3.30 GHz,3.13.0-32-generic i3-3220 LTS(Trusty Tahr)上。

2 个答案:

答案 0 :(得分:121)

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

如果您使用性能计数器检查而不是挂钟时间,您会看到每个延迟循环的核心时钟周期保持不变,确认它只是DVFS的影响(所有现代CPU都使用它)在大多数情况下以更高能效的频率和电压运行。

如果您使用Skylake的内核支持new power-management mode (where the hardware takes full control of the clock speed)进行测试,则加速发生的速度会快得多。

如果你让它在Intel CPU with Turbo上运行一段时间,一旦热限制要求时钟速度降低到最大持续频率,你可能会看到每次迭代的时间再次增加。

引入usleep 会阻止Linux's CPU frequency governor提高时钟速度,因为即使在最低频率下,该过程也不会产生100%的负载。 (即内核的启发式决定CPU运行得足够快,以便在其上运行的工作负载。)

对其他理论的评论

re:David's theory that a potential context switch from usleep could pollute caches:这一般不是一个坏主意,但它无法解释这段代码。

缓存/ TLB污染对此实验来说并不重要。时间窗口内部基本上没有任何内容触及除堆栈末尾之外的内存。大多数时间花在一个微小的循环(1行指令缓存)上,只接触一个int堆栈内存。 usleep期间任何潜在的缓存污染只是此代码的一小部分时间(实际代码会有所不同)!

x86的更多细节:

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

run函数可能位于与main不同的缓存行中(因为gcc将main标记为“冷”,因此它的优化程度较低,并与其他冷函数/数据一起放置)。我们可以期待一两个instruction-cache misses。但是,它们可能仍然在同一个4k页面中,因此main将在进入程序的定时区域之前触发潜在的TLB未命中。

gcc -O0会将OP的代码编译为something like this (Godbolt Compiler explorer):将循环计数器保留在堆栈的内存中。

空循环将循环计数器保留在堆栈内存中,因此在典型的Intel x86 CPU上,循环在OP的IvyBridge CPU上每~6个循环运行一次,这要归功于{转发延迟属于{ {1}}具有内存目标(读取 - 修改 - 写入)。 add是600k个周期,它主导了最多几个缓存未命中的贡献(每次约200个周期用于代码获取未命中,这会阻止进一步的指令发布直到它们被解决)。

无序执行和存储转发应该主要隐藏访问堆栈时潜在的高速缓存未命中(作为100k iterations * 6 cycles/iteration指令的一部分)。

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

答案 1 :(得分:3)

usleep的调用可能会也可能不会导致上下文切换。如果是这样,它将花费更长的时间,而不是它没有。