c++ - 为什么这个延迟循环在没有 sleep 的几次迭代后开始运行得更快?

标签 c++ linux performance benchmarking

考虑:

#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,具有 3.13.0-32-通用 Ubuntu 14.04.1 LTS(可信赖的塔尔)。

最佳答案

在 26 次迭代后,Linux 将 CPU 提升到最大时钟速度,因为您的进程使用了​​它的全部 time slice连续几次。

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

如果您在 Skylake 上进行测试内核支持new power-management mode (where the hardware takes full control of the clock speed) ,加速会发生得更快。

如果你让它在 Intel CPU with Turbo 上运行一段时间,一旦热限制要求时钟速度降低到最大持续频率,您可能会看到每次迭代的时间再次略有增加。 (请参阅 Why can't my CPU maintain peak performance in HPC 了解更多关于 Turbo 让 CPU 运行速度快于其在高功率工作负载下所能承受的速度的信息。)


引入 usleep 可防止 Linux's CPU frequency governor提高时钟速度,因为即使在最低频率下,该过程也不会产生 100% 的负载。 (即内核的启发式方法决定 CPU 运行得足够快,足以应付在其上运行的工作负载。)



对其他理论的评论:

回复:David's theory that a potential context switch from usleep could pollute caches : 总的来说,这不是一个坏主意,但它无助于解释这段代码。

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

关于 x86 的更多详细信息:

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

run 函数可能与 main 位于不同的缓存行中(因为 gcc 将 main 标记为“冷”,因此它得到了优化少并与其他冷功能/数据一起放置)。我们可以期待一两个 instruction-cache misses .不过,它们可能仍在同一个 4k 页面中,因此 main 将在进入程序的计时区域之前触发潜在的 TLB 未命中。

gcc -O0 会将 OP 的代码编译为 something like this (Godbolt Compiler explorer) : 将循环计数器保存在堆栈的内存中。

空循环将循环计数器保存在堆栈内存中,因此典型的 Intel x86 CPU由于存储转发延迟是具有内存目标(读取-修改-写入)的 add 的一部分,因此循环在 OP 的 IvyBridge CPU 上每 6 个周期运行一次迭代。 100k 迭代 * 6 周期/迭代 是 600k 周期,它支配了最多几个缓存未命中的贡献(大约 200 个周期用于代码获取未命中,这会阻止进一步的指令发出,直到它们被解决)。

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

即使循环计数器保存在寄存器中,100k 个周期也很多。

关于c++ - 为什么这个延迟循环在没有 sleep 的几次迭代后开始运行得更快?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/38299023/

相关文章:

Linux mkdir、查找和 mv

linux - bash 脚本中的一个特定命令在由 cron 运行时不起作用,所有其他命令都可以

.net - XAML 性能 : Set alpha channel of a brush vs. FrameworkElement 的不透明度

c++ - 具有嵌套类型的类的概念

c++ - 在优先队列中随机访问

C++ STL算法(列表排序)OpenMP/多线程实现

linux - 将 plink 剂量 (.raw) 格式转换为 ped 格式

性能调整 Hive 查询

java - 相等测试顺序会影响 Java 的性能吗?

c++ - 无法链接 ipopt 库(MacOS Mojave 上的 C++)