【问题标题】:Why does this loop take 1.32 cycles per iteration为什么这个循环每次迭代需要 1.32 个周期
【发布时间】:2019-12-19 10:15:24
【问题描述】:

考虑这个简单的 C++ 函数来计算数组的prefix sum

void prefix_sum(const uint32_t* input, uint32_t* output, size_t size) {
    uint32_t total = 0;
    for (size_t i = 0; i < size; i++) {
        total += input[i];
        output[i] = total;
    }
}

循环 compiles 到 gcc 5.5 上的以下程序集:

.L5:
        add     ecx, DWORD PTR [rdi+rax*4]
        mov     DWORD PTR [rsi+rax*4], ecx
        add     rax, 1
        cmp     rdx, rax
        jne     .L5

我没有看到任何可以阻止它在每次迭代中运行 1 个周期的东西,但我始终在 Skylake i7-6700HQ 上以 1.32 (+/- 0.01) 周期/迭代来测量它,当它针对 8 KiB 运行时输入/输出数组。

循环在 uop 缓存之外提供服务,不会跨越任何 uop 缓存边界,性能计数器也不会指示任何前端瓶颈。

它是 4 fused uops1,这个 CPU 可以维持 4 fused ops/cycle。

有通过ecxrax的依赖链,每个1个周期,但是这些add uop可以去4个ALU端口中的任何一个,所以看起来不太可能发生冲突。融合的cmp 需要转到 p6,这更令人担忧,但我只测量到 p6 的 1.1 uops/迭代。这可以解释每次迭代 1.1 个周期,但不是 1.4 个。如果我以 2 倍端口压力展开循环,则要低得多:所有 p0156 小于 0.7 微秒,但每次迭代 1.3 个周期时性能仍然出乎意料地慢。

每次迭代有一个存储,但我们可以每个周期进行一个存储。

每次迭代有一个负载,但我们每个周期可以做两个。

每个周期有两个复杂的 AGU,但我们每个周期可以做两个。

这里的瓶颈是什么?

有趣的是,我尝试了 Ithermal performance predictor,它几乎完全正确:估计 1.314 个周期与我测量的 1.32 个周期。


1 我通过 uops_issued.any 计数器确认了宏和微融合融合,该计数器在融合域中计数,并在此循环中每次迭代读取 4.0 个融合微指令。

【问题讨论】:

  • 您是否检查了 4k 混叠?如果你有一个方便的 MCVE 调用程序,我会在我的桌面上测试运行它。
  • @PeterCordes 我检查了ld_blocks_partial.address_alias 报告的数字很低,并且不会随着问题的大小而增加。两个阵列都与 2 MiB 对齐。是的,我应该提供一个 MCVE,但是由于当前的基准测试分布在十几个文件中,所以这有点工作,但我会在某个时候完成它。
  • @HadiBrais:在 27 亿个循环中,我得到了 CYCLE_ACTIVITY.STALLS_MEM_ANY:u 的 250 万个计数。所以它不高但非零。 (不限于用户空间,大约为 4.2M)。但是resource_stalls.sb:u 大约是 70k 到 90k 并且嘈杂,降低了约 30 倍。所以商店瓶颈可能只是噪音。
  • 我想知道是否存在某种寄存器读取限制。例如agner.org/optimize/blog/read.php?i=415#857 还展示了读取更多寄存器(或使用复杂寻址模式?)会减慢 Skylake。因此,我的更改带来的加速可能是由于从循环条件中消除了一个寄存器。
  • 我注意到每次迭代的 p4 计数高于 1 并且接近于循环/迭代,即可以解释大部分性能差异。例如,原始版本的展开版本以 1.26 个周期/迭代运行,并显示 1.25 uops/迭代到 p4。表示可能因为它们的操作数没有准备好而正在重放存储?不过,这更有可能是症状而不是原因。

标签: c++ optimization x86 intel micro-optimization


【解决方案1】:

我刚刚使用了有关 Ithermal 性能预测器的说明,我可能已经发现了问题。 试试看

add     ecx, DWORD PTR [rdi]
mov     DWORD PTR [rsi], ecx
add     rax, 1
cmp     rdx, rax

每次迭代提供惊人的 1.131 个周期。在每次迭代中添加 0 进行交叉检查(再次给出 1.3 个周期)消除了存储/加载瓶颈的可能性。 这最终表明寻址模式存在问题。

(编者注:这是一个有趣的实验数据,与我在 Agner Fog 博客上的帖子中发布的内容相匹配,下面的猜测被误解了。更简单的寻址模式可以加快速度,即使没有分层。)


(编者注:这部分是错误的:我们从问题中知道没有未分层,因为uops_issued.any = 4 每次迭代。)

我认为在索引寻址的情况下,您的 CPU 会取消您的 add/mov 分层。这种行为在几个架构(SnB、SKL、HWL)中都有很好的记录,有人在描述整个事情的 stackoverflow 上做得很好:https://stackoverflow.com/a/31027695/1925289 简而言之:如果涉及的寄存器和标志过多,则融合的操作 (DSB) 会变得未分层 (IDQ),从而有效地再次取消融合。

其他资源:

【讨论】:

  • BeeOnRope 在问题中说,他使用性能计数器确认循环是 4 个融合域微指令。这样就排除了不分层。这也不是我在 Agner Fog 博客线程上的帖子的内容,而是关于未融合域 uop 吞吐量 限制和/或寄存器读取吞吐量限制。没有限制多少融合是可能的。我在 HSW 和 SKL 上都发现减少输入寄存器的数量是有帮助的,这表明还有一些其他未知的微架构限制,就像你通过阅读更少的 regs 所证明的那样。
  • 所以是的,复杂的寻址模式是个问题,但可能只是因为每个微指令的额外输入。也可能是因为依赖于最近增加的 RAX,但不太可能。无论如何,我们知道 HSW 和 SKL 可以保持那些 add+load 和 mov-store 微融合,指令之外的上下文不会影响这一点。
  • 在 DSB 之后进行非层压。你确定 uops_issued.any 重要吗?
  • @PeterCordes - 我怀疑这里涉及到寄存器读取限制(正如您在 Agner 的博客中所描述的那样)。首先,似乎没有读取足够的寄存器,并且如果您展开 2 倍,效果仍然存在(但更小)。使用 2x 展开,读取的寄存器肯定不会很多,并且所需的 IPC 大约是 3 而不是 4,这也有助于消除“太多 uops”理论(如 unlamination 理论)。一般来说,与预期的 1.0 个周期/迭代相比,展开会不断减少 delta,即使在 4 倍展开时,它仍然是 1.07 个迭代/周期(ish)。
  • 我想提醒未来的读者,赏金在这里是自动分配的,作为(唯一)获得最多投票的答案,但它没有回答这个问题。赏金分配不是背书。
猜你喜欢
  • 2019-03-26
  • 1970-01-01
  • 2021-10-14
  • 2023-01-30
  • 2016-07-04
  • 1970-01-01
  • 2017-02-12
  • 1970-01-01
  • 2020-09-01
相关资源
最近更新 更多