以下汇编指令有什么好处吗?

5
在我们的系统编程课程中,我们正在学习汇编语言。在大多数示例程序中,我们的教授展示了以下内容:
XOR CX, CX

替代

MOV CX, 0

或者

OR AX, AX
JNE SOME_LABEL

替代

CMP AX, 0
JNE SOME_LABEL

或者

AND AL, 0FH        ; To convert input ASCII value to numeral
; The value in AL has already been checked to lie b/w '0' and '9'

替代

SUB AL, '0'

我的问题是,使用AND/ORXOR是否比使用替代方法(易于理解/阅读)性能更好?
由于这些程序通常在理论课程中展示给我们,大多数同学无法口头评估它们。为什么要花费40分钟的讲座来解释这些琐碎的语句呢?

5
指令可能会更短,且不会产生空字节。 - Kerrek SB
2
...还有一些特殊的优化,比如寄存器重命名,可以识别xor eax,eax - Ben Jackson
6个回答

6
XOR CX, CX  ;0x31 0xC9

只使用两个字节:操作码0x31和ModR/M字节,存储源寄存器和目标寄存器(在这种情况下,这两个寄存器相同)。

MOV CX, 0  ;0xB8 0x08 0x00 0x00

需要更多的字节:操作码0xB8,ModR/M用于目标寄存器(在此情况下为CX),并填充了两个字节的立即数零。

从时钟的角度来看没有区别(都只需要一个时钟周期),但是mov需要4个字节,而xor仅使用2个字节。

OR AX, AX  ;0x0A 0xC0

再次仅使用操作码字节和ModRM字节,而

CMP AX, 0  ;0x3D 0x00 0x00 <-- but usually 0x3B ModRM 0x00 0x00

使用三个或四个字节。在本例中,它使用三个字节(操作码0x3D,表示零的立即数),因为x86对于某些累加器寄存器操作有特殊的操作码,但通常会使用四个字节(操作码、ModR/M、立即数)。同样,在谈论CPU时钟时也是如此。

对于处理器执行没有任何区别。

AND AL, 0x0F  ;0x24 0x0F  <-- again special opcode for Accumulator

并且

SUB AL, '0'  ;0x2D 0x30 0x00  <-- again special opcode for Accumulator

只有一个字节的差异,在你减去ASCII零时,不能确定累加器中不会保留大于9的值。同时,按位与操作将OFCF设置为零,而sub根据结果设置它们,按位与操作可能更安全,但我个人认为这取决于上下文。


2
CMP AX, 0 不会使用4个字节;即使是不同的寄存器, cmp si, 0 也会使用 cmp r/m16, sign_extended_imm8。只有 movtest 没有用于更宽操作数大小的符号扩展imm8格式。除非你告诉汇编器愚蠢并且不使用最短的编码方式。另外,and al, imm8sub al, imm8 都是2个字节。(https://www.felixcloutier.com/x86/sub显示了 2C ib 编码。对于 sub ax, '0',您选择了2D imm16编码。)请参阅 Tips for golfing in x86/x64 machine code - Peter Cordes
mov cx, 0 不使用 ModR/M 字节。 - ecm

4
除了其他答案中提到的代码大小节省外,我想提一些你可以在Intel优化手册Agner Fog的x86优化指南中了解更多的内容:
现代x86处理器将XOR REG,REGSUB REG,REG(其中REG为两个操作数均相同)识别为依赖项断开器;这意味着它们还有一个目的,即打破先前寄存器/标志值上的虚假依赖关系。请注意,如果您清除8位或16位寄存器,则不一定适用,但如果您清除32位寄存器,则会适用。
OR AX, AX
JNE SOME_LABEL

我认为首选指令应该是TEST AX,AX。在现代x86处理器上,TEST可以与任何条件跳转宏融合(基本上与跳转指令组合成一个单独的指令),而CMP只能与无符号条件跳转融合,至少在Nehalem架构之前是这样。同样,我不确定对于16位操作数是否也是这种情况。

1
mov 也会打破对寄存器先前值的依赖关系。它只在 xor 等情况下被提到,因为在一般情况下输出确实取决于先前的值,因此需要特殊支持来识别该情况。movzxmovd 等都将目标寄存器的其余部分清零,从而打破了依赖链(与 pinsrwmovlhps 相反)。 - Peter Cordes

2
  1. 关于如何将x86汇编中的寄存器清零,重复了这个问题。最好的方法是使用xor指令。虽然在现代CPU上,对于小于32位的寄存器,大多数优点并不适用,但至少早期的P6系列CPU如果将CX与ECX分别重命名,则仍会特殊处理xor cx,cx,以避免写入CL后读取CX时出现部分寄存器停顿。但代码大小优势总是适用的。

  2. 关于使用CMP reg,0 vs OR reg,reg测试寄存器是否为零,重复了这个问题。在某些CPU上,使用or ax,ax比使用test ax,ax效率低,因为test指令专门设计用于此目的。使用or似乎是8080的遗留问题。它们都比cmp ax,0节省一个字节的代码大小,但所有这些指令都以相同的方式设置FLAGS(请参阅我链接的答案以了解8080的ora a惯用法)。

  3. 这里没有使用AND的优势。它们都是相同的代码大小(2个字节)。使用AND可以提醒您ASCII数字的低4位是整数值。通常情况下,sub al,'0'更有用,因为您可以将其作为检查字符是否为数字的一部分。例如:sub al,'0' / cmp al,9 / ja non-digit,否则您会在寄存器中得到整数值。在那里使用and作为第一步总是会创建0..15范围内的结果,从而产生许多误报。请参见NASM Assembly convert input to integer?以获取用例:停止于第一个非数字字符的循环。另请参见What is the idea behind ^= 32, that converts lowercase letters to upper and vice versa?关于ASCII上的范围检查。


1
一个重要的区别是它们是否影响CPU操作标志。当您使用逻辑操作xoror等时,操作标志会受到影响。因此:
XOR  CX, CX

这不仅会将 CX 清零,还会设置 CPU 的零标志。 mov 指令不影响标志位。因此:

MOV  CX, 0

不会设置零标志,例如。

在使用 xor 后,什么时候需要使用 ZF? - user35443
2
@user35443,如果您正在检查标志位,并且该点可能是从代码中的多个位置到达的,则可能需要这样做。因此,进行检查的地方可能不知道先前影响标志位的指令是 xor - lurker

1
除了之前提到的指令调度,哪个指令更快也可能取决于正在执行的实际指令序列。
在GMP著名的Torbjörn Granlund的this paper第8页上,有一个看似无害的指令对性能产生了巨大影响。在页面右上角的示例三中,一个非常快速的除法循环从“nop”指令开始。根据同一页上的脚注4,缺少nop指令会导致循环执行慢1个时钟周期。Granlund建议尝试在循环内放置其他nop以实现进一步加速。
我最初的直觉反应是指令越多,时间越长。然而,指令调度和执行显然比手册中所能得出的更为复杂。

这可能更好地对齐复杂/简单解码器的后续指令。Core2早于循环缓存(Nehalem)和uop缓存(Sandybridge),因此即使对于短循环,解码器吞吐量也是一个因素。 - Peter Cordes

-2

XOR 操作比 MOV 操作更快,因为它是一种按位操作,所有按位操作都可以由 CPU 更快地执行。


哎?为什么要使用移位器来实现异或操作? - Michael
我本意是要写按位运算,抱歉我的错误。 - StrawhatLuffy
2
这不是真的。mov reg, imm和xor reg, reg都只需要一个时钟周期。 - user35443

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接