考虑:
#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)。
在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
的调用可能会导致上下文切换,也可能不会导致上下文切换。 如果有的话,所花的时间会比没有更长。