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

phy*_*xnj 71 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;
}
Run Code Online (Sandbox Code Playgroud)

这是示例代码.在定时循环的前26次迭代中,run函数花费大约0.4ms,但随后成本降低到0.2ms.

usleep取消注释时,延迟循环对于所有运行都需要0.4 ms,从不加速.为什么?

代码是使用g++ -O0(无优化)编译的,因此延迟循环不会被优化掉.它运行在Intel(R)Core(TM)i3-3220 CPU @ 3.30 GHz,3.13.0-32通用Ubuntu 14.04.1 LTS(Trusty Tahr)上.

Pet*_*des 122

在26次迭代之后,Linux将CPU提升到最大时钟速度,因为您的进程连续几次使用其全时间片.

如果您使用性能计数器而不是挂钟时间进行检查,您会发现每个延迟循环的核心时钟周期保持不变,这证实它只是DVFS的影响(所有现代CPU都使用它来运行更多能量 -大多数时候有效的频率和电压).

如果您在具有内核支持的Skylake上测试新的电源管理模式(硬件完全控制时钟速度),则可以更快地进行加速.

如果你在带有TurboIntel CPU上运行一段时间,一旦热限制要求时钟速度降低到最大持续频率,你可能会看到每次迭代的时间再次略微增加.


引入ausleep可以防止Linux的CPU频率调节器提高时钟速度,因为即使在最低频率下,该过程也不会产生100%的负载.(即内核的启发式决定CPU的运行速度足以支持在其上运行的工作负载.)



对其他理论的评论:

re:大卫的理论认为潜在的上下文切换usleep会污染缓存:这一般不是一个坏主意,但它无助于解释这段代码.

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

更详细的x86:

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

run函数可能位于不同的高速缓存行中main(因为gcc标记main为"冷",因此它被优化得更少并且与其他冷函数/数据一起放置).我们可以期待一个或两个指令缓存未命中.但是,它们可能仍处于相同的4k页面中,因此main在进入程序的定时区域之前会触发潜在的TLB未命中.

gcc -O0会将OP的代码编译成类似这样的东西(Godbolt Compiler explorer):将循环计数器保存在堆栈的内存中.

空循环将循环计数器保留在堆栈存储器中,因此在典型的Intel x86 CPU上,循环在OP的IvyBridge CPU上每6个循环运行一次,这要归功于add存储目的地的存储转发延迟(读取) -modify写). 100k iterations * 6 cycles/iteration是600k个周期,它主导了最多几个高速缓存未命中的贡献(每个约200个周期用于代码提取未命中,这阻止了进一步的指令发布直到它们被解决).

无序执行和存储转发应该主要隐藏访问堆栈时潜在的高速缓存未命中(作为call指令的一部分).

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