当前位置: 首页 > 知识库问答 >
问题:

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

弘康安
2023-03-14

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

第一个:

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-states驱动程序是intel_pstate。C状态通过使用处理器修复。max_cstate=1 intel_idle。max_cstate=0。频率调节器设置为性能(cpupower frequency set-g performance),并且禁用了turbo(

共有1个答案

衡修洁
2023-03-14

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

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

这就是为什么您永远不想使用-O0调试模式进行真正的基准测试:瓶颈与常规优化不同(至少-O2-最好是

在英特尔Sandybridge-family(包括@ unbalance _ mark的Kaby Lake CPU)上,如果重新加载不试图在存储后立即运行,而是在几个周期后运行,则存储转发延迟会更低。添加一个冗余赋值在没有优化的情况下编译时加速代码,并且使用函数调用的循环比空循环更快,这两者都在未优化的编译器输出中证明了这种效果。

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

或者可能是存储缓冲区的静态分区加快了存储转发?尝试在另一个核心上运行微创循环可能会很有趣,如下所示:

// 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();
    }
}

在Skylake上暂停块大约100个周期,而在早期的CPU上大约为5个周期。

因此,如果存储转发的好处是来自必须发出/执行的其他线程的uops,则此循环将执行更少的操作,并且运行时将更接近于它在单线程模式下拥有物理内核时。

但是,如果好处仅仅是分区ROB和存储缓冲区(这可能会加快负载探测存储的时间),我们仍然会看到全部好处。

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

也许用完BOB(分支顺序缓冲区)槽是阻止其他线程的分支微操作发送到无序后端的主要机制。现代x86 CPUs在检测到分支预测失误时,会对RAT和其他后端状态进行快照,以实现快速恢复,允许回滚到预测失误的分支,而无需等待其达到报废状态。

这避免了在分支之前等待独立工作,并在恢复时让无序执行它。但这意味着更少的分支可以在飞行中。至少更少的条件/间接分支?IDK,如果直接 jmp 将使用 BOB 条目;其有效性是在解码过程中建立的。所以也许这个猜测是站不住脚的。

< code>while(1){}循环中没有本地变量,因此不会成为存储转发的瓶颈。这只是一个< code>top: jmp top循环,每次迭代可以运行1个周期。这是英特尔的一条单微操作指令。

i5-8250U是一个Kaby Lake,与Coffee Lake不同,它的循环缓冲区(LSD)仍然被Skylake等微码禁用。因此,它不能在LSD/IDQ(为问题/重命名阶段提供队列)中展开自己,必须在每个周期从uop缓存中分别获取jmpuop。但IDQ确实可以缓冲,只需要每4个周期执行一次发布/重命名周期,就可以为该逻辑核心发布一组4个jmp uop。

但无论如何,在SKL/KBL上,这两个线程加在一起不仅会使uop缓存获取带宽饱和,而且会以这种方式相互竞争。在启用LSD(环回缓冲区)的CPU上(例如Haswell/Broadwell或Coffee Lake等),它们不会。Sandybridge/Ivybridge不会展开微小的循环来使用更多的LSD,所以你在那里也会有同样的效果。我不确定这是否重要。在哈斯韦尔或咖啡湖进行测试会很有趣。

(无条件的< code>jmp总是结束一个uop缓存行,而且它也不是一个跟踪缓存,所以一个uop缓存提取不能给你一个以上的< code>jmp uop。)

我必须纠正我上面的确认:我把所有程序都编译成C (g ),这给出了大约2%的差异。如果我把所有东西都编译成C,我会得到大约8%,接近OPs的大约10%。

有趣的是,< code>gcc -O0和< code>g -O0编译循环的方式不同。这是GCC的C与C前端向GCC的后端提供不同的GIMPLE/RTL或类似的东西的一个怪癖,并且< code>-O0没有使后端修复低效率。这不是C与C的根本区别,也不是你能从其他编译器那里得到的。

C版本仍然转换为惯用的do{}while()style循环,在循环的底部,内存目标添加之后,有一个cmp/jle。(此Godbolt编译器资源管理器链接的左窗格)。为什么循环总是编译成“do…while”样式(尾部跳转)?

但是C版本使用< code>if(break)风格的循环,条件在顶部,然后是内存目的地add。有趣的是,仅通过一条< code>jmp指令将内存目标< code>add与< code>cmp reload分开,就能产生如此大的差异。

# 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)

显然,添加/ cmp背靠背使此版本在Skylake / Kaby /咖啡湖上受到较慢的商店转发的影响更大

与这个影响不大的:

# 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:

< code>cmp [mem],imm / jcc可能仍然是微熔丝和/或宏熔丝,但我忘了是哪种。IDK,如果这是相关的,但如果循环是更多的微操作,它不能发出一样快。尽管如此,由于每5或6个周期1次迭代的执行瓶颈(内存-目标< code >添加延迟),前端很容易领先于后端,即使它必须与另一个超线程竞争。

 类似资料:
  • 问题内容: 我想要一个小的“应用程序加载器”程序,该程序可以通过TCP从外部服务器接收其他二进制应用程序文件并运行它们。 我可以通过将传输的文件保存到硬盘上并使用system()调用来运行它来完成此操作。但是,我想知道是否有可能从内存中启动新应用程序而不接触硬盘驱动器。 加载新应用程序后,加载程序应用程序的状态无关紧要。我更喜欢使用C,但是也欢迎使用C ++解决方案。我还要坚持使用标准Linux

  • 问题内容: 这是一个要阐述的问题:为什么说内核在进程地址空间中? 这可能是一个愚蠢的问题,但在我脑海中浮现出来。有关进程地址空间和虚拟内存布局的所有文字都提到进程地址空间具有为内核保留的空间。例如,在32位系统上,进程地址空间为4GB,其中1 GB为Linux中的内核保留(其他OS上可能有所不同)。 我只是想知道为什么当进程无法直接寻址内核时,为什么说内核位于进程地址空间中。为什么我们不说内核具有

  • 当我输入一个字符串运算符时,无论是加法()、减法(-)、乘法(*)、除法(/)还是模块(%),即使我输入了一个有效的输入,它仍然会进入而循环。我不知道问题可能是什么,因为当循环工作正常时,我必须为变量Num2输入一个int值。

  • 我的程序每次要处理某件事情时都会分叉,在每个分叉进程中,我都分离一个线程,以便从分叉进程中记录统计数据:这个线程循环收集数据,但它没有停止这个循环的实际条件。 程序的输出证实了每一个线程都随其工艺而死 当我使用运行这个程序时,引起了一些疑问:当每个分叉进程死亡时,会显示一些令人毛骨悚然的输出(13534是分叉进程PID): 相同的错误(警告?)每个分叉进程死亡时的消息。

  • 问题内容: 我有以下问题。我编写了一个函数,该函数将列表作为输入并为列表中的每个元素创建一个字典。然后,我想将此字典追加到新列表中,以便获得字典列表。我正在尝试为此生成多个进程。我在这里的问题是,我希望不同的进程访问字典列表,因为它由其他进程更新,例如,一旦达到一定长度,就打印一些东西。 我的例子是这样的: 现在我的问题是每个过程都创建自己的过程。有没有一种方法可以在进程之间共享列表,以便所有字典

  • 我有一个节点类,它包含两个嵌套类,服务器和客户机,以及指向每个类实例的指针。 我的猜测是上下文切换正在发生,因此每个进程都有自己的副本被加载到相同的地址中。 我如何改变这一点,使每个进程访问该节点的完全相同的副本? 更新: