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