我对 CPU 时间的理解是,在同一台机器上的每次执行之间,它应该总是相同的。它应该每次都需要相同数量的 cpu 周期。
但是我现在正在运行一些测试,执行一个基本的 echo "Hello World",它给了我 0.003 到 0.005 秒的时间。
我对 CPU 时间的理解是错误的,还是我的测量有问题?
我对 CPU 时间的理解是,在同一台机器上的每次执行之间,它应该总是相同的。它应该每次都需要相同数量的 cpu 周期。
但是我现在正在运行一些测试,执行一个基本的 echo "Hello World",它给了我 0.003 到 0.005 秒的时间。
我对 CPU 时间的理解是错误的,还是我的测量有问题?
你的理解是完全错误的。在现代 CPU 上运行现代操作系统的真实世界计算机并不是简单的理论抽象。有各种因素会影响执行代码所需的 CPU 时间。
考虑内存带宽。在典型的现代机器上,在机器内核上运行的所有任务都在竞争对系统内存的访问。如果代码同时运行,另一个内核上的代码正在使用大量内存带宽,这可能会导致访问 RAM 需要更多时钟周期。
许多其他资源也是共享的,例如缓存。假设代码经常被中断以让其他代码在核心上运行。这将意味着代码会经常发现缓存冷并且发生大量缓存未命中。这也将导致代码占用更多时钟周期。
让我们也谈谈页面错误。代码本身可能在内存中,也可能在代码开始运行时不在。即使代码在内存中,您也可能会或可能不会出现软页面错误(更新操作系统对正在积极使用的内存的跟踪),具体取决于该页面上次发生软页面错误的时间或加载时间进入内存。
您的基本 hello world 程序正在对终端进行 I/O。所花费的时间可能取决于当时与终端交互的其他内容。
对现代系统的最大影响包括:
grep MHz /proc/cpuinfo
)。因此,即使周期是固定的(实际上它们并非如此),您也不会看到相同的时间。
您的假设并非完全错误,但它仅适用于单个循环的核心时钟周期,并且仅适用于不涉及任何内存访问的情况。 (例如,L1d 缓存中的数据已经很热,CPU 内核中的 L1i 缓存中的代码已经很热)。并假设在定时循环运行时没有发生中断。
运行整个程序的操作规模要大得多,并且将涉及共享资源(以及可能的资源争用),例如访问主内存。正如@David 指出的那样,在终端模拟器上write
打印字符串的系统调用- 如果您的程序最终等待它,与另一个进程的通信可能会很慢并且涉及唤醒另一个进程。重定向到或常规文件会删除它,或者只是关闭标准输出,就像会让你的系统调用返回(在 Linux 上)。/dev/null
./hello >&-
write
-EBADF
现代 CPU 是非常复杂的野兽。您大概有一个 Intel 或 AMD x86-64 CPU 的乱序执行,以及十几个用于传入/传出缓存线的缓冲区,允许它跟踪许多未完成的缓存未命中(内存级并行性)。每个核心有 2 级私有缓存和一个共享的 L3 缓存。祝你好运,预测除了最受控制的条件之外的任何东西的准确时钟周期数。
但是,是的,如果您确实控制了条件,那么每次迭代通常会以相同数量的核心时钟周期运行相同的小循环。
然而,即便如此,也并非总是如此。我见过这样的情况,即同一个循环似乎有两个稳定状态,用于 CPU 调度指令的方式。不同的进入条件怪癖可能导致数百万次循环迭代的持续速度差异。
在对现代英特尔 CPU(如 Sandybridge 和 Skylake)进行微基准测试时,我偶尔会看到这种情况。即使借助性能计数器和https://agner.org/optimize,通常也不清楚这两个稳定状态到底是什么,以及究竟是什么导致了瓶颈
在我记得的一种情况下,中断倾向于使循环进入有效的执行模式。@BeeOnRope 正在使用 RDPMC 测量慢周期/迭代很短的时间间隔(或者可能是核心时钟固定的 RDTSC = TSC 参考时钟),而我通过使用非常大的重复计数并仅使用 perf stat 来测量它运行得更快整个程序(这是一个静态可执行文件,只有一个循环用 asm 手工编写)。并且@Bee 能够通过增加迭代计数来重现我的结果,因此在定时区域内会发生中断,并且从中断返回往往会使 CPU 脱离非最佳 uop 调度模式,无论它是什么。