为什么这个延迟循环在没有睡眠的几次迭代后开始变得更快运行?
Why does this delay-loop start to run faster after several iterations with no sleep?
考虑:
#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 毫秒。
当 usleep
未注释时,延迟循环在所有 运行 中花费 0.4 毫秒,从不加速。为什么?
代码是用 g++ -O0
编译的(没有优化),所以延迟循环没有被优化掉。在 Intel(R) Core(TM) i3-3220 CPU @ 3.30 GHz, with 3.13.0-32-generic Ubuntu 14.04.1 LTS (Trusty Tahr) 上 运行。
调用 usleep
可能会也可能不会导致上下文切换。如果有,比没有需要的时间要长。
经过 26 次迭代后,Linux 将 CPU 提高到最大时钟速度,因为您的进程连续几次使用其全部 time slice。
如果您检查性能计数器而不是挂钟时间,您会发现每个延迟循环的核心时钟周期保持不变,确认这只是 DVFS 的效果(所有现代CPU 大部分时间使用更节能的频率和电压 运行。
如果您在 Skylake with kernel support for the new power-management mode (where the hardware takes full control of the clock speed) 上进行测试,启动速度会更快。
如果你把它 运行ning 放在 Intel CPU with Turbo, you'll probably see the time per iteration increase again slightly once thermal limits require the clock speed to reduce back down to the maximum sustained frequency. (See 上一段时间以获得更多关于 Turbo 的信息,让 CPU 运行 比它可以承受的高功率更快工作量。)
引入 usleep
可防止 Linux's CPU frequency governor 提高时钟速度,因为即使在最低频率下,该过程也不会产生 100% 的负载。 (即内核的启发式决定 CPU 运行ning 足够快以应对其上 运行ning 的工作负载。)
对其他理论的评论:
回复::总的来说这不是一个坏主意,但它无助于解释这段代码。
缓存/TLB 污染对于这个实验来说根本不重要。除了堆栈的末尾之外,在时间 window 内基本上没有任何内容触及内存。大部分时间都花在一个只触及一个 int
堆栈内存的小循环(1 行指令缓存)中。 usleep
期间任何潜在的缓存污染只是此代码时间的一小部分(实际代码会有所不同)!
关于 x86 的更多详细信息:
对 clock()
的调用本身可能会缓存未命中,但代码获取缓存未命中会延迟开始时间测量,而不是成为测量内容的一部分。对 clock()
的第二次调用几乎不会延迟,因为它在缓存中应该仍然很热。
run
函数可能在与 main
不同的缓存行中(因为 gcc 将 main
标记为 "cold",所以它得到的优化较少并与其他冷 functions/data)。我们可以期待一两个 instruction-cache misses。不过,它们可能仍在同一个 4k 页面中,因此 main
将在进入程序的定时区域之前触发潜在的 TLB 未命中。
gcc -O0 会将 OP 的代码编译为 something like this (Godbolt Compiler explorer):将循环计数器保存在堆栈的内存中。
空循环将循环计数器保存在堆栈内存中,因此在典型的 Intel x86 CPU 循环中 运行s 在 OP 的 IvyBridge CPU 上每 ~6 个周期进行一次迭代,由于存储转发延迟是 add
的一部分,带有内存目标(读-修改-写)。 100k iterations * 6 cycles/iteration
是 600k 周期,它最多占几个缓存未命中的贡献(每次代码获取未命中大约 200 个周期,这会阻止发出进一步的指令,直到它们被解决)。
乱序执行和存储转发应该主要隐藏访问堆栈时潜在的缓存未命中(作为 call
指令的一部分)。
即使循环计数器保存在寄存器中,100k 个周期也很多。
考虑:
#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 毫秒。
当 usleep
未注释时,延迟循环在所有 运行 中花费 0.4 毫秒,从不加速。为什么?
代码是用 g++ -O0
编译的(没有优化),所以延迟循环没有被优化掉。在 Intel(R) Core(TM) i3-3220 CPU @ 3.30 GHz, with 3.13.0-32-generic Ubuntu 14.04.1 LTS (Trusty Tahr) 上 运行。
调用 usleep
可能会也可能不会导致上下文切换。如果有,比没有需要的时间要长。
经过 26 次迭代后,Linux 将 CPU 提高到最大时钟速度,因为您的进程连续几次使用其全部 time slice。
如果您检查性能计数器而不是挂钟时间,您会发现每个延迟循环的核心时钟周期保持不变,确认这只是 DVFS 的效果(所有现代CPU 大部分时间使用更节能的频率和电压 运行。
如果您在 Skylake with kernel support for the new power-management mode (where the hardware takes full control of the clock speed) 上进行测试,启动速度会更快。
如果你把它 运行ning 放在 Intel CPU with Turbo, you'll probably see the time per iteration increase again slightly once thermal limits require the clock speed to reduce back down to the maximum sustained frequency. (See
引入 usleep
可防止 Linux's CPU frequency governor 提高时钟速度,因为即使在最低频率下,该过程也不会产生 100% 的负载。 (即内核的启发式决定 CPU 运行ning 足够快以应对其上 运行ning 的工作负载。)
对其他理论的评论:
回复:
缓存/TLB 污染对于这个实验来说根本不重要。除了堆栈的末尾之外,在时间 window 内基本上没有任何内容触及内存。大部分时间都花在一个只触及一个 int
堆栈内存的小循环(1 行指令缓存)中。 usleep
期间任何潜在的缓存污染只是此代码时间的一小部分(实际代码会有所不同)!
关于 x86 的更多详细信息:
对 clock()
的调用本身可能会缓存未命中,但代码获取缓存未命中会延迟开始时间测量,而不是成为测量内容的一部分。对 clock()
的第二次调用几乎不会延迟,因为它在缓存中应该仍然很热。
run
函数可能在与 main
不同的缓存行中(因为 gcc 将 main
标记为 "cold",所以它得到的优化较少并与其他冷 functions/data)。我们可以期待一两个 instruction-cache misses。不过,它们可能仍在同一个 4k 页面中,因此 main
将在进入程序的定时区域之前触发潜在的 TLB 未命中。
gcc -O0 会将 OP 的代码编译为 something like this (Godbolt Compiler explorer):将循环计数器保存在堆栈的内存中。
空循环将循环计数器保存在堆栈内存中,因此在典型的 Intel x86 CPU 循环中 运行s 在 OP 的 IvyBridge CPU 上每 ~6 个周期进行一次迭代,由于存储转发延迟是 add
的一部分,带有内存目标(读-修改-写)。 100k iterations * 6 cycles/iteration
是 600k 周期,它最多占几个缓存未命中的贡献(每次代码获取未命中大约 200 个周期,这会阻止发出进一步的指令,直到它们被解决)。
乱序执行和存储转发应该主要隐藏访问堆栈时潜在的缓存未命中(作为 call
指令的一部分)。
即使循环计数器保存在寄存器中,100k 个周期也很多。