【问题标题】:Are RMW instructions considered harmful on modern x86?RMW 指令是否被认为对现代 x86 有害?
【发布时间】:2016-10-28 07:39:08
【问题描述】:

我记得在优化 x86 以提高速度时,通常要避免读取-修改-写入指令。也就是说,你应该避免像add [rsi], 10 这样的东西,它会增加存储在rsi 中的内存位置。建议通常是将其拆分为 read-modify 指令,然后是 store,因此类似于:

mov rax, 10
add rax, [rsp]
mov [rsp], rax

或者,您可以使用显式加载和存储以及 reg-reg 添加操作:

mov rax, [esp]
add rax, 10
mov [rsp], rax

对于现代 x86,这仍然是合理的建议(曾经是这样吗?)?1

当然,如果内存中的值被多次使用,RMW 是不合适的,因为您会产生冗余的加载和存储。我对一个值只使用一次的情况感兴趣。

基于在 Godbolt 中的探索,所有 icc、clang 和 gcc prefer 使用单个 RMW 指令来编译类似:

void Foo::f() {
  x += 10;
}

进入:

Foo::f():
    add     QWORD PTR [rdi], 10
    ret

所以至少大多数编译器似乎认为 RMW 没问题,因为该值只使用一次。

有趣的是,当增量值是全局而不是成员时,各种编译器同意,例如:

int global;

void g() {
  global += 10;
}

在这种情况下,gccclang 仍然是单个 RMW 指令,而 icc prefers 是带有显式加载和存储的 reg-reg add:

g():
        mov       eax, DWORD PTR global[rip]                    #5.3
        add       eax, 10                                       #5.3
        mov       DWORD PTR global[rip], eax                    #5.3
        ret     

也许这与RIP 相对寻址和微融合限制有关?但是,icc13 仍然对-m32 做同样的事情,所以可能更多的是与需要 32 位位移的寻址模式有关。


1我故意使用模糊的术语现代 x86 基本上是指英特尔和 AMD 笔记本电脑/台式机/服务器芯片的最后几代。

【问题讨论】:

  • 您查看过 Agner Fog 的微架构指南吗? RIP-relative 寻址模式在某种程度上可能很特别,但我认为它们有时可以微融合。但通常,IIRC,内存目标操作最多与单独的加载/修改/存储指令相同数量的微指令。 IIRC 对于 Broadwell/Skylake 上的 adc 而言并非如此。
  • icc 可能偏爱单独的指令,因此它们都可以是单指令。 Godbolt的icc13是旧的;许可问题使得提供更新版本是否合法尚不清楚。它可能正在优化没有 uop 缓存的 CPU 上的解码速度,其中不是解码组中第一个的多 uop 指令将降低解码器吞吐量。 (OTOH,解码组中第一个的多微指令增加了解码吞吐量,因为 Core2/Nehalem 解码器最多可以解码 4-1-1-1,这与 SnB 系列不同)。
  • 我稍后可能会将其转换为答案,但现在这只是一个快速评论,无需查找任何内容,只是略过问题。
  • icc 可能试图在同一条指令中避免 disp32 + immediate(请参阅我对问题的编辑;它与 -m32 做同样的事情)。 disp + imm 对微融合 IIRC 有影响。或者至少可能需要 uop 缓存中的多个条目,即使就 ROB 而言它实际上是单个融合域 uop。虽然我认为这只是 disp32+imm32 的问题,而不仅仅是 disp32 + imm8。无论如何,我可能会在 Agner 的手册中挖掘这些东西,但您可以根据这些 cmets 作为起点自己挖掘它,并将其写成一个不错的答案。
  • Darek Mihocka 不久前用各种架构对此进行了测试;你可以在他的博客here 上读到他的面条(关于 Pentium 4 和 x86 的一般问题的一系列引人入胜的文章的一部分)。不同的编译器有不同的代码生成策略,但原因是性能调优复杂。没有一刀切的策略。如果您真的关心,您必须对您的目标架构进行基准测试。

标签: assembly optimization x86 intel


【解决方案1】:

RMW 指令是否被认为对现代 x86 有害?

没有。

在现代 x86/x64 上,输入指令被翻译成微指令。
任何 RMW 指令都会被分解成多个微指令;事实上,这些微指令将被分解成相同的微指令。

通过使用“复杂”RMW 指令而不是单独的“简单”读取、修改和写入指令,您可以获得以下效果。

  1. 需要解码的指令更少。
  2. 更好地利用指令缓存
  3. 更好地利用可寻址寄存器

您可以在Agner Fog's instruction tables 中清楚地看到这一点。

ADD [mem],const 有 5 个周期的延迟。

MOV [mem],reg 和反之亦然,每个延迟为 2 个周期,ADD reg,const 延迟为 1,总共为 5。

我查看了 Intel Skylake 的时序,但 AMD K10 是一样的。

您需要考虑到编译器必须迎合许多不同的处理器,有些编译器甚至为不同的处理器系列使用相同的核心逻辑。这可能导致相当不理想的策略。

RIP 相对寻址
在 X64 RIP 上,相对寻址需要一个额外的周期来解决旧处理器上的 RIP。
Skylake 没有这种延迟,我相信其他人也会消除这种延迟。
我确定您知道 x86 不支持 EIP 相对寻址;在 X86 上,您必须以一种迂回的方式执行此操作。

【讨论】:

  • @PeterCordes 我计时了 1e7 次迭代 20 add m, r/i + dec + jnz。我得到了this。 2.2e8 指令问题,以及 4.1e8 fused uops。 2e8 加载、存储数据和存储地址发生。端口 4 处理 100% 的存储数据,端口 7 处理 55% 的存储地址,端口 2 和 3 分别处理 50% 的负载和 22.5% 的存储地址。端口 6 处理 100% 的 dec+taken 分支 (1e7)。剩下的是 2.4e8 微指令在端口 0、1、5、6 上拆分,所有 2e8 add 的 1 ALU 微指令没有完全解释。
  • @IwillnotexistIdonotexist:IIRC,如果一组 4 人已准备好但 RS 或 ROB 已满,则没有 uops 问题是正常的。我想我以前见过。所以,是的,当 OOO 核心已满时,前端会停止,而不是涓涓细流。对于问题(每次迭代 41 个融合域 uops,因此每次添加 2 个)和退休(每个迭代 81 个未融合域 uops,因此每次添加 4 个)而言,计数看起来很棒。
  • @PeterCordes HSW 一如既往。是的,我指的是某个地方神秘的额外 25% 的工作量。我又跑了一次,同时记录 p0156 和 p2347:同样的事情。 p0156 给了58578120+60002738+62237010+72324777=253142645
  • @IwillnotexistIdonotexist:至少看起来可以重现。我很难想出任何合理的猜测来解释。也许OOO核心可以重新执行uops或在内部生成新的uops? (不让它们退役?)如果我们通过循环一个 giant 数组将存储转发完全排除在外怎么办。循环计数显然将由缓存/TLB 未命中和 COW 页面错误主导,但我只想要 ALU uop 计数。 (可能单字节添加将与双字相同,减少不重用情况下的内存占用)。
  • @IwillnotexistIdonotexist:如果您有时间,您是否还可以测试来自 LSD 的 uop 吞吐量,用于测试 Haswell CPU 上不是 4 uop 倍数的小循环? BeeonRope 有这个in the answer to this question 的测试代码。我们拥有的唯一数据点是 Skylake(在循环缓冲区内展开或其他内容)并且对于 SnB 不完整(一个迭代的第一个 uop 不能与前一个的最后一个 uop 在同一组中发出)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-09-09
  • 2011-12-28
  • 1970-01-01
  • 2014-11-24
  • 2010-12-22
  • 2010-11-08
  • 1970-01-01
相关资源
最近更新 更多