【问题标题】:What's the easiest way to determine if a register's value is equal to zero or not?确定寄存器的值是否等于零的最简单方法是什么?
【发布时间】:2017-05-01 16:30:30
【问题描述】:

我将 x86 程序集与 Irvine 库一起使用。

检查寄存器值是否为零的最简单方法是什么?

我使用 cmp 指令,但我正在寻找替代方法。 这是我使用 cmp 指令的代码,寄存器是 ebx

    cmp ebx,0
    je equ1
    mov ebx,0
    jmp cont
equ1:
    mov ebx,1
    jmp cont
cont:
    exit

这“布尔化”一个值,产生一个 0 或 1,就像 C 中的 int ebx = !!ebx 一样。

【问题讨论】:

  • 您至少使用了 80386 条指令(ebx 在 8086 上不存在)。答案也取决于此代码之前的指令,零值的存在可能会被先前的指令“泄漏”,从而在测试中为您节省更多,否则常见的成语是test ebx,ebx 设置 ZF。然后在 ebx 中将 ZF 变为 0/1 值是另一回事,test 可能再次不是最佳解决方案的一部分。下定决心,是否要确定寄存器是否为零,或者是否要根据它将某个寄存器设置为0/1。 # mov ebx,0 = xor ebx,ebx 什么时候可以销毁标志。
  • 为了说明我的观点(细节的重要性),假设前面的最后一条(标志和 ebx)修改指令是 neg ebx。然后sbb ebx,ebx 将在ebx 等于0 时将ebx 设置为-1,对于非零值设置为0(进一步neg ebx 将把它变成你原来的0/1 工作方式)。跨度>
  • 不适合我。
  • 顺便说一句,我在neg 之后得到了sbb 的结果。如果为零,它将保持零,对于非零,它将执行-1 ...所以要返回您的 0/1然后inc ebx 会做。 # “没有和我一起工作” 我也没有。 (我不知道你在说什么)
  • 我可以发誓我们有这个问题的副本,但我没有找到它。但是,我确实发现了一堆隐含包含您问题的答案的问题,例如stackoverflow.com/questions/147173/…stackoverflow.com/questions/13064809/the-point-of-test-eax-eax。它们都是关于test 当两个操作数都是同一个寄存器时所做的事情——这正是你想要的。 test ebx, ebx + jz

标签: assembly x86 micro-optimization


【解决方案1】:

对于如何确定的“最简单”或“不关心细节”的答案可能是:

    ; here ebx is some value, flags are set to anything
    test   ebx,ebx   ; CF=0, ZF=0/1 according to ebx
    jz     whereToJumpWhenZero
    ; "non-zero ebx" will go here

    ; Or you can use the inverted "jnz" jump to take
    ; a branch when value was not zero instead of "jz".

有一个详细的answer from Peter Cordes to "testl eax against eax?" question 推理关于设置标志等。还有一个链接到另一个类似的答案,但从性能的角度推理为什么它是最好的方法。 :)

如何在ebx 为零时将其他一些寄存器(我将选择eax)设置为1,当ebx 为非零时设置为0(ebx 本身的非破坏性方式):

    xor   eax,eax  ; eax = 0 (upper 24 bits needed to complete "al" later)
    test  ebx,ebx  ; test ebx, if it is zero (ZF=0/1)
    setz  al       ; al = 1/0 when ZF=1/0 (eax = 1/0 too)

或者当ebx 为零/非零时如何将ebx 本身转换为1/0:

    neg   ebx      ; ZF=1/0 for zero/non-zero, CF=not(ZF)
    sbb   ebx,ebx  ; ebx = 0/-1 for CF=0/1
    inc   ebx      ; 1 when ebx was 0 at start, 0 otherwise

或者当ebx 为零/非零时如何将ebx 本身转换为1/0,其他变体(在“P6”到​​“Haswell”核心上更快):

    test  ebx,ebx  ; ZF=1/0 for zero/non-zero ebx
    setz  bl       ; bl = 1/0 by ZF (SETcc can target only 8b r/m)
    movzx ebx,bl   ; ebx = bl extended to 32 bits by zeroes

等,等等...这取决于您的测试之前发生的情况,以及您真正想要的测试输出,有许多可能的方法(针对不同情况是最佳的,对于不同的情况是最佳的)不同的目标 CPU)。


我将添加一些更常见的情况...从 N 到零的计数器循环向下计数,循环 N 次:

    mov   ebx,5   ; loop 5 times
exampleLoop:
    ; ... doing something, preserving ebx
    dec   ebx
    jnz   exampleLoop ; loop 5 times till ebx is zero

如何处理word(16b)数组的5个元素(以数组[0]、数组[1]、...顺序访问它们):

    mov   ebx,-5
    lea   esi,[array+5*2]
exampleLoop:
    mov   ax,[esi+ebx*2]  ; load value from array[i]
    ; process it ... and preserve esi and ebx
    inc   ebx
    jnz   exampleLoop  ; loop 5 times till ebx is zero

再举一个例子,不知怎的,我很喜欢这个:

ebx 为零/非零并且您在某个寄存器中已经有值1ecx 在示例):

    ; ecx = 1, ebx = some value
    cmp   ebx,ecx  ; cmp ebx,1 => CF=1/0 for ebx zero/non-zero
    sbb   eax,eax  ; eax = -1 (~0) / 0 for CF=1/0 ; ebx/ecx intact

-1 可能看起来和 1 一样实用(至少用于索引目的),但 -1 也可以作为完整位掩码用于进一步的 and/xor/or 操作,因此有时它更方便。

【讨论】:

  • 感谢它正常工作 :) 感谢您的帮助。
  • 对于不使用任何额外寄存器的相同寄存器情况:test ebx,ebx / setz bl / movzx ebx, bl 在 P6 到 Haswell 上应该更有效(SBB 是 2c 延迟 / 2 uop)。 neg/sbb/inc 在 Pentium4(慢 setcc)上效率更高,我认为它们在 Broadwell 及更高版本以及 AMD(所有 1c 延迟)上是相等的,并且代码大小更小(尤其是在 32 位中)。跨度>
  • test/setz/movzx 是 gcc6.2 -mtune=haswell 所做的,如果你给它一个想要就地执行的情况:godbolt.org/g/076BSI。 Clang 将一个单独的寄存器归零,然后使用一个 mov。
  • @PeterCordes 谢谢,作为另一个变体添加到答案中......加上强调“许多”..;):D......(但实际上,如果有人需要最佳“设置 ebx 到1/0 当零/非零”,可能有办法改变算法的另一部分以获得更好的性能,而没有ebx = ebx ? 0 : 1; :)) .. 除了一些与 sgn(number) 实现类似的好奇心,它有一些好的也在这里回答,我想你是在 1-2 个月前就开始了 :)。
  • 奇怪的是,英特尔的编译器实际上总是在这种情况下使用CMOV 指令,即使在我看来是交替的指令序列(就像@Peter 建议的那样)会更优化。因此,要么英特尔工程师有特殊信息表明条件移动实际上总是更好,要么优化器只是将其作为固定路径编写。 (或者这可能是他们坚持使用 AMD 的方式之一,因为 AMD 处理器往往具有稍高的条件移动延迟。)
【解决方案2】:

使用旗帜,卢克
您可以通过检查零标志来测试寄存器是否为零。
如果寄存器通过影响标志(或更具体地说,零标志)的某些操作获得其值,那么您不必做任何事情,因为零标志已经反映了存储在该寄存器中的值。

仅在需要时进行测试
如果您不能保证已设置标志,则必须使用测试操作。
这些操作有两种形式:破坏性和非破坏性。

您可以在以下位置查看指令列表及其更改的标志:http://ref.x86asm.net——更具体地说,是:http://ref.x86asm.net/coder32-abc.html

movlea 指令从不改变标志,因此需要帮助。大多数其他指令设置至少一个标志。

不要创建错误的依赖项
如果您需要测试寄存器是否为零,但又不想更改其值,请使用test 指令。
您不应该使用orand 指令来检查寄存器,因为CPU 可能不知道or/and 可以非破坏性地使用并且可能无法应用某些优化。 对此的技术术语是“错误依赖”。该寄存器需要 ebx 并且“认为”它最近已更改,因此它等待结果最终确定。

test ebx, ebx  ; <-- CPU knows ebx was not altered, no stalls on subsequent reads.
or   ebx, ebx  ; <-- CPU 'thinks' ebx was changed, stall on subsequent read.

如果您希望零状态反映在另一个寄存器中,您可以简单地将mov ebx 映射到另一个寄存器。

将值减少为布尔值
如果要将寄存器减少为布尔值(如果非零则为 True,否则为 False),请使用以下序列之一:

; ebx holds the value to reduce to a boolean.
; eax is an unused register.
xor eax, eax   ; eax = 0
sub eax, ebx   ; eax = 0 - ebx; CF (carry flag) = 1 if ebx <> 0
sbb ebx, ebx   ; ebx = ebx - ebx - CF
               ; <<-- ebx = -1 if non zero, 0 if zero
xor eax, eax   ; eax = 0
sub eax, ebx   ; eax = - ebx; CF = 1 if ebx <> 0
adc ebx, eax   ; ebx = (ebx + -ebx) aka 0 + CF
               ; <<== ebx = 1 if non zero, 0 if zero
test ebx, ebx  ; force ZF to be correct
setnz al       ; Store 1 if non-zero, 0 otherwise in byte register AL.

请注意,由于与“部分寄存器写入”相关的停顿,使用字节寄存器可能会出现问题。

【讨论】:

  • 正如 Ped7g 的回答所指出的,neg ebx 在一条指令中设置了来自0 - ebx 的标志(但也修改了原始值)。
【解决方案3】:

你可以使用:

or ebx, 0 ;This does nothing, just triggers the zero flag if ebx is zero
jnz notZero 
or ebx, 1 ;ebx was zero, then ebx is 1
notZero:

【讨论】:

  • 直接投反对票or ebx,0,即使我已经在评论中发表了常见的成语是test ebx,ebx。您至少可以使用or ebx,ebx 来避免立即使用0,但是test 仍然更好,因为它被C/C++ 编译器广泛使用,因此CPU 可以对此进行额外的优化,而@987654328 @ 可以被视为普通的算术运算。
  • 是的,我在发布我的帖子后看到了你的答案,学到了一些东西!谢谢!
  • @Ped7g:更多关于为什么or 是一个糟糕的选择:stackoverflow.com/a/33724806/224132。总结:除了明显的代码大小之外,test ebx,ebx 可以与 JCC 进行宏融合,并且它不写入 EBX,从而在依赖链中引入了一个额外的延迟周期,以便下一次读取 EBX。所以是的,对于 TEST/JCC 与 OR/JCC,CPU 确实“有额外的优化”,并且 OR 的处理方式与任何其他 OR 相同。
猜你喜欢
  • 2010-09-06
  • 1970-01-01
  • 2011-01-13
  • 2010-11-06
  • 1970-01-01
  • 1970-01-01
  • 2023-03-08
  • 2014-02-24
  • 2012-03-14
相关资源
最近更新 更多