提问者:小点点

为什么这个延迟循环在没有Hibernate的情况下经过几次迭代后开始运行得更快?


考虑:

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

usleee被取消注释时,延迟循环的所有运行时间为0.4 ;ms,从不加速。 为什么?

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


共2个答案

匿名用户

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

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

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

如果您让它在Intel CPU上运行一段时间,一旦热限制要求时钟速度降低到最大持续频率,您可能会看到每次迭代的时间再次略微增加。 (请参阅“为什么我的CPU不能在HPC中保持峰值性能”,以了解更多关于Turbo的信息,Turbo让CPU的运行速度超过了它在高功率工作负载下所能承受的速度。)

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

对其他理论的评论:

RE:David的理论,即从usleee中进行的潜在上下文切换可能会污染缓存:一般来说,这是个不错的主意,但它并不能帮助解释这段代码。

Cache/TLB污染对本实验来说根本不重要。 在计时窗口中,除了堆栈的末尾之外,基本上没有任何东西会触及内存。 大部分时间都花在一个微小的循环中(1行指令缓存),只触及堆栈内存的一个int。 在usleee期间任何潜在的缓存污染都是这段代码时间的极小一部分(真正的代码会有所不同)!

有关x86的更多详细信息:

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

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

GCC-O0将把OP的代码编译成这样(Godbolt Compiler explorer):将循环计数器保存在堆栈的内存中。

空循环将循环计数器保留在堆栈内存中,因此在典型的Intel x86 CPU上,循环在OP的IvyBridge CPU上每6个周期运行一次迭代,这要归功于存储转发延迟,这是add与内存目的地(读-修改-写)的一部分。 100k迭代*6周期/迭代是600k周期,这最多是一对缓存未命中(每个约200个周期用于代码获取未命中,这会阻止发出更多的指令,直到它们被解决)。

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

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

匿名用户

usleee的调用可能会导致上下文切换,也可能不会导致上下文切换。 如果有的话,所花的时间会比没有更长。