为什么当另一个进程共享同一个 HT 核心时,一个进程的执行时间更短

Why is execution time of a process shorter when another process shares the same HT core

我有一个 Intel CPU,有 4 个 HT 内核(8 个逻辑 CPUs),我构建了两个简单的进程。

第一个:

int main()
{
  for(int i=0;i<1000000;++i)
    for(int j=0;j<100000;++j);
}

第二个:

int main()
{
  while(1);
}

两者都是用 gcc 编译的,没有特殊选项。 (即默认 -O0:无优化调试模式,将变量保存在内存中而不是寄存器中。)

当我运行第一个逻辑上的第一个CPU(CPU0),而当其他逻辑CPU上的负载电荷接近0 %,这第一个进程的执行时间是:

real    2m42,625s
user    2m42,485s
sys     0m0,070s

然而,当我 运行 CPU4 上的第二个进程(无限循环)时(CPU0 和 CPU4 在同一个核心上但不是在同一个硬件线程上),第一个进程的执行时间是

real    2m25,412s
user    2m25,291s
sys     0m0,047s

我预计需要更长的时间,因为同一个核心上有两个进程,而不是只有一个。但它实际上更快。 为什么会这样?

编辑: P 状态驱动程序是 intel_pstate。使用 processor.max_cstate=1 intel_idle.max_cstate=0 固定 C 状态。 频率调节器设置为性能 (cpupower frequency-set -g performance) 并禁用涡轮 (cat /sys/devices/system/cpu/intel_pstate/no_turbo 给出 1)

Both are compiled with gcc without special options. (I.e. with the default of -O0: no optimization debug mode, keeping variables in memory instead of registers.)

与普通程序不同,具有 int i,j 循环的版本完全解决了存储转发延迟的瓶颈,而不是前端吞吐量或后端执行资源或任何共享资源。

这就是为什么您永远不想使用 -O0 调试模式进行真正的基准测试:瓶颈与正常优化 不同-O2 在最少,最好 -O3 -march=native).


在英特尔 Sandybridge 系列(包括@uneven_mark 的 Kaby Lake CPU)上,存储转发延迟 更低如果重新加载不会在存储后立即尝试 运行,而是在几个周期后 运行。 and also 都在未优化的编译器中证明了这种效果输出。

让另一个超线程竞争前端带宽显然使这种情况有时会发生。

或者存储缓冲区的静态分区可以加速存储转发?在另一个核心上尝试微创循环 运行ning 可能会很有趣,像这样:

// compile this with optimization enabled
// and run it on the HT sibling of the debug-mode nested loop
#include  <immintrin.h>

int main(void) {
    while(1) {
      _mm_pause(); _mm_pause();
      _mm_pause(); _mm_pause();
    }
}

pause 在 Skylake 上阻塞了大约 100 个周期,高于之前 CPUs 的大约 5 个周期。

因此,如果存储转发的好处是来自另一个必须 issue/execute 的线程的微指令,则此循环将做更少的事情并且 运行-time 将更接近它的时间在单线程模式下有一个物理内核。

但如果收益仅来自于对 ROB 和存储缓冲区进行分区(这可能会加快负载探测存储的时间),我们仍然会看到全部收益。

更新:@uneven_mark 在 Kaby Lake 上进行测试,发现这将 "speedup" 从 ~8% 降低到~2%。因此,显然争夺前端/后端资源是无限循环的重要组成部分,它可以阻止另一个循环过早地重新加载。

也许用完 BOB(分支顺序缓冲区)槽是阻止其他线程的分支 uops 发出到乱序后端的主要机制。现代 x86 CPUs 对 RAT 和其他后端状态进行快照,以便在它们检测到分支预测错误时允许快速恢复,允许回滚到错误预测的分支,而无需等待它退休。

这避免了在分支之前等待独立工作,并在恢复时继续乱序执行。但这意味着可以运行的分支更少。至少 conditional/indirect 个分支? IDK 如果直接 jmp 将使用 BOB 条目;它的有效性在解码期间建立。所以这个猜测可能站不住脚。


while(1){} 循环在循环中没有局部变量,因此它不会在存储转发上出现瓶颈。它只是一个 top: jmp top 循环,每次迭代可以 运行 1 个循环。那是 Intel 上的单 uop 指令。

i5-8250U is a Kaby Lake, and (unlike Coffee Lake) still has its loop buffer (LSD) disabled by microcode like Skylake. So it can't /IDQ(为 issue/rename 阶段提供队列)并且每个周期都必须从 uop 缓存中单独获取 jmp uop。但是 IDQ 确实缓冲了这一点,每 4 个周期只需要一个 issue/rename 周期来为该逻辑核心发出一组 4 个 jmp 微指令。

但是无论如何,在 SKL/KBL 上,这两个线程一起超过饱和 uop 缓存获取带宽,并且确实以这种方式相互竞争。在启用了 LSD(环回缓冲区)的 CPU 上(例如 Haswell / Broadwell,或 Coffee Lake 及更高版本),他们不会。 Sandybridge/Ivybridge 不要展开微小的循环来使用更多的 LSD,这样你就会有同样的效果。我不确定这是否重要。 在 Haswell 或 Coffee Lake 上进行测试会很有趣。

(一个无条件的 jmp 总是结束一个 uop-cache 行,而且它不是跟踪缓存,所以一个 uop-cache 获取不能给你超过一个 jmp uop。)


I have to correct my confirmation from above: I compiled all programs as C++ (g++), which gave the roughly 2% difference. If I compile everything as C, I get about 8%, which is closer to OPs roughly 10%.

这很有趣,gcc -O0g++ -O0 确实以不同的方式编译循环。这是 GCC 的 C 与 C++ 前端向 GCC 的后端提供不同 GIMPLE/RTL 或类似内容的一个怪癖,并且 -O0 没有让后端修复效率低下的问题。 这不是关于 C 与 C++ 的任何基本内容,也不是您对其他编译器的期望。

C 版本仍然转换为惯用的 do{}while() 样式循环,在循环底部有一个 cmp/jle,在内存目标之后 right添加。 (this Godbolt compiler explorer link).

上的左窗格

但 C++ 版本使用 if(break) 循环样式,条件位于顶部,然后内存目标添加。 有趣的是,仅通过一个 jmp 指令将内存目标 addcmp 重新加载分开,这会产生很大的不同。

# inner loop, gcc9.2 -O0.   (Actually g++ -xc but same difference)
        jmp     .L3
.L4:                                       # do {
        add     DWORD PTR [rbp-8], 1       #   j++
.L3:                                  # loop entry point for first iteration
        cmp     DWORD PTR [rbp-8], 99999
        jle     .L4                        # }while(j<=99999)

显然 add/cmp 背靠背使这个版本在 Skylake / Kaby/Coffee Lake

对比这个没有受到太大影响:

# inner loop, g++9.2 -O0
.L4:                                      # do {
        cmp     DWORD PTR [rbp-8], 99999
        jg      .L3                         # if(j>99999) break
        add     DWORD PTR [rbp-8], 1        # j++
        jmp     .L4                       # while(1)
.L3:

cmp [mem], imm / jcc 可能还是 micro and/or macro-fuse,但我忘记是哪个了。 IDK 如果这是相关的,但如果循环更多 uops,它就不能那么快发出。尽管如此,由于每 5 或 6 个周期 1 次迭代的执行瓶颈(内存目标 add 延迟),前端很容易保持领先于后端,即使它必须与另一个超线程竞争.