【问题标题】:Which is generally faster to test for zero in x86 ASM: "TEST EAX, EAX" versus "TEST AL, AL"?在 x86 ASM 中测试零通常更快:“TEST EAX,EAX”与“TEST AL,AL”?
【发布时间】:2023-03-25 21:05:01
【问题描述】:

在 AL 中测试零/非零字节通常哪个更快?

  • TEST EAX, EAX
  • TEST AL, AL

假设之前的 "MOVZX EAX, BYTE PTR [ESP+4]" 指令加载了一个字节参数,其扩展名为 EAX 的其余部分,以防止我已经知道的组合值惩罚。

所以 AL=EAX 并且没有读取 EAX 的部分寄存器惩罚。

仅凭直觉检查 AL 可能会让您认为它更快,但我敢打赌对于 >32 位寄存器的字节访问需要考虑更多惩罚问题。

感谢任何信息/细节,谢谢!

【问题讨论】:

  • 你的意思是movzx eax, [esp+4]。检查eaxal 没有速度差异。注意你也可以test byte [esp+4], 0xff
  • 糟糕,是的,已更正/编辑!谢谢!
  • 我很确定性能上的差异,如果有的话,将无法衡量,因为它会淹没在噪音中。并且绝对与用户可见的任何内容无关。
  • 如果您写了al 并阅读了eax,您会遇到麻烦,但仅限于SandyBridge 之前的CPU。使用movzx 才是真正的诀窍。
  • @SevaAlekseyev:如果有差异,我们可以设计一个微基准测试,至少通过性能计数器来衡量它。您可以将test 放入带有setcccmovcc 的数据依赖链中,而不是jcc,以验证它是否仍有1 个周期延迟。

标签: performance assembly x86 micro-optimization


【解决方案1】:

代码大小相同,所有 x86 CPU AFAIK 上的性能也相同。

Intel CPU(带有部分寄存器重命名)在写入 EAX 后绝对不会因为读取 AL 而受到惩罚。其他 CPU 也不会因为读取低字节寄存器而受到惩罚。

读取 AH 会对 Intel CPU 造成不利影响,例如一些额外的延迟。 (How exactly do partial registers on Haswell/Skylake perform? Writing AL seems to have a false dependency on RAX, and AH is inconsistent)

通常 32 位操作数大小和 8 位操作数大小(低 8 位而不是高位 8 位)的速度相同,除了 写入的错误依赖性或后来的部分寄存器读取惩罚 em> 一个 8 位寄存器。因为 TEST 只读取寄存器,所以这不是问题。即使add al, bl 也很好:该指令已经对两个寄存器都有输入依赖性,并且在 Sandybridge 系列中,寄存器低字节的 RMW 不会单独重命名它。 (无论如何,Haswell 和以后都不会单独重命名低字节寄存器)。

选择您喜欢的任何操作数大小。 8位和32位基本相等。选择只是人类可读性的问题。如果您稍后要将该值作为 32 位整数使用,则使用 32 位。如果它在逻辑上仍然是一个 8 位值,并且您只使用 movzx 作为 ARM ldrb 或 MIPS lbu 的 x86 等效项,那么使用 8 位是有意义的。

cmp al, imm 这样可以使用 no-modrm 短格式编码的指令有代码大小的优势。 cmp al, 0一些 旧 CPU(Core 2)上仍然比 test al,al 差,其中 cmp/jcc 宏融合不如 test/jcc 宏融合灵活。 (Test whether a register is zero with CMP reg,0 vs OR reg,reg?)


这些指令之间有一个区别:test al,al 根据 AL 的高位(可以是非零)设置 SFtest eax,eax 将始终清除 SF。如果您只关心 ZF,那没有什么区别,但是如果您将 SF 中的高位用于以后的分支或 cmovcc/setcc,那么您可以避免执行第二次test


其他测试内存字节的方法:

如果您使用 setcc 或 cmovcc 而不是 jcc 分支来使用标志结果,那么宏融合在下面的讨论中无关紧要。

如果您以后还需要寄存器中的实际值,movzx/test/jcc 几乎肯定是最好的。否则,您可以考虑进行内存目的地比较。

cmp [mem], immediate 可以在 Intel 上微融合成 load+cmp uop,只要寻址模式不是 RIP 相关的。 (在 Sandybridge 系列中,索引寻址模式即使在 Haswell 和更高版本上也不会分层:参见 Micro fusion and addressing modes)。 Agner Fog 没有提到 AMD 是否有将 cmp/jcc 与内存操作数融合的限制。

;;; no downside for setcc or cmovcc, only with JCC on Intel
;;; unknown on AMD
    cmp byte [esp+4], 0       ; micro-fuses into load+cmp with this addressing mode
    jnz   ...                 ; breaks macro-fusion on SnB-family

我没有 AMD CPU 来测试当 cmp 为 mem, immediate 时 Ryzen 或任何其他 AMD 是否仍然融合 cmp/jcc。现代 AMD CPU 通常做 cmp/jcc 和 test/jcc 融合。 (但不能像 SnB 家族那样添加/sub/and/jcc 融合)。

cmp mem,imm/jcc(对比movzx/test+jcc):

  • 更小的代码大小(以字节为单位)
  • 在主流英特尔上具有相同数量的前端/融合域微指令 (2)。如果 cmp+load 的微融合是不可能的,那么这将是 3 个前端 uops,例如使用 RIP 相对寻址模式 + 立即。或者在具有索引寻址模式的 Sandybridge 系列上,它会在解码后但在发送到后端之前解压到 3 uop。

    优势:在 Silvermont/Goldmont / KNL 或没有宏融合的非常旧的 CPU 上,这仍然是 2。 movzx/test/jcc 的主要优势在于宏融合,因此它在不会发生这种情况的 CPU 上落后。

  • 3 个后端 uops(未融合域 = 调度程序中的执行端口和空间,即 RS),因为 cmp-immediate 无法与英特尔 Sandybridge 系列 CPU 上的 JCC 进行宏融合(在 Skylake 上测试)。 uop 是 load、cmp 和单独的分支 uop。 (movzx / test+jcc 为 2)。后端 uops 通常不是直接的瓶颈,但如果负载暂时没有准备好,它会占用 RS 中的更多空间,从而限制了这种无序执行可以看到的更远距离。

cmp [mem], reg / jcc 可以宏 + 微融合成一个比较 + 分支微指令,非常棒。如果您在函数后面需要一个归零寄存器,请先对其进行异或归零,然后将其用于内存上的单微指令比较+分支。

    movzx  eax, [esp+4]       ; 1 uop (load-port only on Intel and Ryzen)
    test   al,al                     ; fuses with jcc
    jnz    ...                ; 1 uop

这仍然是前端的 2 个微指令,但后端也只有 2 个。 test/jcc 宏融合在一起。不过,它会花费更多的代码大小。

如果您不是分支而是使用cmovccsetcc 的FLAGS 结果,则使用cmp mem, imm 没有任何缺点。只要您不使用 RIP 相对寻址模式(当还有立即数时总是阻止微融合)或索引寻址模式,它就可以进行微融合。

【讨论】:

  • "Agner Fog 没有提到这个限制,因为 AMD 有这个限制,而 cmp 有一个内存操作数" 这句话有些狡猾。读起来不好。
  • @SepRoland:谢谢,已修复。我一定是在句子中间改变了主意,我的手指不同步了。
猜你喜欢
  • 1970-01-01
  • 2017-01-26
  • 1970-01-01
  • 2013-02-17
  • 1970-01-01
  • 2012-10-15
相关资源
最近更新 更多