一尘不染

为什么该延迟环在无hibernate的几次迭代后开始运行得更快?

linux

考虑:

#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(无需优化)编译的,因此不会优化延迟循环。它可以在3.30 GHz的Intel®CoreTM
i3-3220

CPU 上运行,并具有3.13.0-32通用的Ubuntu
14.04.1

LTS(Trusty
Tahr)。


阅读 228

收藏
2020-06-02

共1个答案

一尘不染

经过26次迭代后,Linux将CPU提升至最大时钟速度,因为您的进程连续两次使用其全部时间片

如果您使用性能计数器而不是挂钟时间进行检查,您会发现每个延迟环的核心时钟周期保持恒定,从而确认这只是DVFS的作用(所有现代CPU都使用DVFS以更高的能耗运行-
大部分时间都是有效的频率和电压)。

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

如果您将它在带有TurboIntel
CPU
上运行一段时间,则可能会发现,一旦散热限制要求时钟速度降低到最大持续频率,每次迭代的时间就会再次稍微增加。(有关Turbo的更多信息,请参见为什么我的CPU无法在HPC中保持峰值性能,更多有关Turbo使CPU的运行速度超过其在高功率工作负载下的承受能力的信息。)


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



缓存/ TLB污染对于该实验根本不重要
。除了堆栈的末尾,时序窗口内基本上没有其他东西可以接触内存。大部分时间都花在一个很小的循环(1行指令高速缓存)中,该循环仅接触int堆栈存储器之一。usleep对于此代码,任何潜在的高速缓存污染时间仅占该代码时间的一小部分(实际代码将有所不同)!

对于x86更详细:

对其clock()自身的调用可能会丢失高速缓存,但是代码获取高速缓存未命中会延迟开始时间的测量,而不是被测量的一部分。clock()几乎永远不会延迟对的第二次调用,因为它在缓存中仍然很热。

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

gcc
-O0会将OP的代码编译为如下代码(Godbolt编译器浏览器):将循环计数器保存在堆栈中的内存中。

空循环将循环计数器保持在堆栈内存中,因此,在典型的Intel x86
CPU上
,循环的运行是在OP的IvyBridge
CPU上每6个周期执行一次迭代,这要归功于add存储目标的一部分的存储转发延迟(读-modify-write)。 100k iterations * 6 cycles/iteration周期为60万个周期,最多可控制几个高速缓存未命中(每个200个周期用于代码提取未命中,这会阻止进一步的指令发出,直到它们被解决为止)。

乱序执行和存储转发应在访问堆栈时(作为call指令的一部分)在大多数情况下隐藏潜在的高速缓存未命中。

即使将循环计数器保存在寄存器中,也要花费100k个周期。

2020-06-02