为什么这个延迟循环在几次迭代之后开始运行得更快而没有睡眠?
考虑:
#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毫秒,但成本却降低到了0.2毫秒。
当没有注意到睡眠时,延迟循环在所有运行中都需要0.4毫秒,从不加速。 为什么?
代码用g++ -O0
编译(不优化),所以延迟循环没有被优化。 它运行在Intel(R)Core(TM) i3-3220 CPU @ 3.30 GHz,带有3.13.0-32-通用Ubuntu 14.04.1 LTS(Trusty Tahr )。
经过26次迭代之后,Linux将CPU加速到最大时钟速度,因为您的进程连续使用了整个时间片 。
如果使用性能计数器而不是挂钟时间进行检查,则会看到每个延迟循环的核心时钟周期保持不变,这证实了这只是DVFS (所有现代CPU都使用更节能的运行)高效的频率和电压大部分时间)。
如果您在Skylake上进行了testing,并且内核支持新的电源pipe理模式(硬件完全控制时钟速度) ,那么启动速度会更快。
如果在运行Turbo的Intel CPU上运行一段时间,一旦热限制需要时钟速度降低到最大持续频率,您可能会看到每次迭代的时间再次略有增加。
引入一个usleep
可以防止Linux的CPU频率调节器提高时钟速度,因为该过程即使在最低频率下也不会产生100%的负载。 (也就是说,内核的启发式决定了CPU运行速度足够快,以适应正在运行的工作负载)。
对其他理论的评论 :
回复: David的理论认为,潜在的从usleep
切换到usleep
可能会污染高速caching :总的来说,这并不是一个坏主意,但它并不能解释这段代码。
caching/ TLB污染对于这个实验根本不重要 。 在时间窗口内部除了堆栈的末端之外几乎没有任何内容触及内存。 大部分时间都花费在一个很小的循环(1行指令高速caching),它只触及堆栈内存的一个int
。 usleep
期间任何潜在的caching污染只是这段代码的一小部分时间(实际代码将会不同)!
更详细的x86:
对clock()
本身的调用可能会导致cache-miss,但是code-cache cache miss延迟了开始时间的测量,而不是被测量的一部分。 clock()
的第二个调用几乎不会被延迟,因为它在caching中应该仍然很热。
run
函数可能在与main
不同的caching行中(因为gcc将main
标记为“cold”,所以它被优化得更less并且与其他冷函数/数据一起放置)。 我们可以预期一个或两个指令caching未命中 。 他们可能仍然在同一个4k页面,但是, main
会在进入节目的定时区域之前引发潜在的TLB缺失。
gcc -O0会将OP的代码编译成像这样的代码(Godbolt Compiler explorer) :将循环计数器保存在堆栈的内存中。
空循环将循环计数器保存在堆栈内存中,因此在典型的Intel x86 CPU上 ,循环运行在OP的IvyBridge CPU上每循环约6次,这归功于存储转发延迟,这是存储器目的地的一部分读 – 修改 – 写)。 100k iterations * 6 cycles/iteration
是600k周期,这决定了至多一对caching未命中的贡献(对于取代未命中,每次约200个循环,防止进一步的指令发出,直到它们被解决)。
无序执行和存储转发应该大部分隐藏访问堆栈时潜在的caching未命中(作为call
指令的一部分)。
即使循环计数器保存在寄存器中,100k个周期也是很多的。
一个叫usleep
的调用可能会或可能不会导致上下文切换。 如果确实如此,则需要花费较长的时间。