使用 x86 样式的 2 操作数指令会破坏其目标,您始终可以使用 mov 模拟非破坏性 3 操作数指令以将一个操作数复制到目标,然后运行破坏性关于该目的地的说明。
# with ecx and edx holding your inputs (which I'm calling C and D).
mov ebx, ecx ; ebx = C
sub ebx, edx ; ebx = C - D
这是您在这种情况下可以做的最好的事情,您不需要破坏 ECX 和 EDX 中的值。
如果您的可用寄存器不足,将 ECX 保存在堆栈中,然后在 ECX 中生成 C - D 结果而不是新寄存器可能是一个不错的选择。
通常您可以在整个函数中为同一个变量继续使用同一个寄存器,但这不是必需的,有时也不是最佳选择。使用 cmets 跟踪事物。
编译器通常非常擅长寄存器分配,但它们的代码可能难以阅读,因为它们甚至不尝试与寄存器使用保持一致。对于非破坏性操作,他们通常会无缘无故地将结果放入新的寄存器中。尽管如此,编译器输出通常是优化的良好起点。 (写一个做某事的小函数,看看它是如何编译的。或者用函数 args 而不是常量作为输入,用 C 编写你的整个东西,然后编译它。)
x86 有一些用于其他操作的复制和操作指令(不是sub),most notably LEA。
lea ebx, [ecx + ecx*4] ; ebx = C * 5
lea ebx, [ecx + ebx - 2] ; ebx = C + D - 2
x86 寻址模式可以加或减常量,但只能左移和加寄存器。
immediate-operand form of imul 也是 3 操作数,用于使用 1 或 2 个 LEA 无法实现的乘法器:
imul ebx, ecx, 0x01010101 ; ebx = cl repeated 4 times, if upper bytes were zero
与大多数立即操作数指令不同,imul 没有overload the /r field in the ModRM byte as extra opcode bits。所以它有空间来编码一个寄存器目标和一个 reg/mem 源,因为186 为它专用了一个完整的操作码字节。
ISA 扩展(例如 BMI1 和 BMI2)添加了一些新的 3 操作数整数指令,例如 ANDN 和 SHRX。
andn ebx, ecx, edx ; ebx = (~C) & D ; BMI1
shrx ebx, edx, ecx ; ebx = D >> C ; BMI2
但它们并不是普遍可用的,只有 Haswell 及更高版本,以及 Ryzen。 (而且 Haswell/Skylake 的 Pentium/Celeron 版本仍然在没有它们的情况下出售,进一步延迟了它们成为基准的时间点。感谢英特尔。)
当然对于向量指令,AVX 提供所有 SSE 指令的非破坏性版本。
movaps xmm2, xmm0 ; copy a whole register
subsd xmm2, xmm1 ; scalar double-precision FP subtract: xmm0-xmm1
vsubsd xmm3, xmm0, xmm1
或者一个不太明显的用例
xorps xmm0, xmm0 ; zero the register and break any false dependencies
cvtsi2sd xmm0, eax ; convert to double-precision FP, with the upper element = 0
xorps xmm1, xmm1
cvtsi2sd xmm1, edx
对比AVX:
vxorps xmm1, xmm1,xmm1 ; xmm1 = all-zero
vcvtsi2sd xmm0, xmm1, eax
vcvtsi2sd xmm1, xmm1, edx
这会重用相同的归零 reg 作为合并目标以避免错误的依赖关系(并使 128 位寄存器的高 64 位为零)。