【问题标题】:Assembly Language - Subtract edx from ecx and place result in ebx汇编语言 - 从 ecx 中减去 edx 并将结果放入 ebx
【发布时间】:2018-07-04 21:20:32
【问题描述】:

如何在不更改任何其他寄存器的情况下执行此操作(又名保持 ecxedx 与以前相同)?

在 C++ 中,应该是这样的:

int ecx = 3;
int edx = 1;
int ebx = ecx - edx;

到目前为止,我已经做到了:

mov ecx, 1
mov edx, 3
sub ecx, edx
mov ebx, ecx

【问题讨论】:

  • 你尝试过做什么?
  • 只需从 ecx 中减去 edx 然后使 ebx 等于 ecx 但是我必须将 ecx 的值改回来
  • 您是否考虑过先将ecx 的值复制到ebx,然后再直接在ebx 上执行sub? (这实际上也是大多数 C++ 编译器会做的事情)
  • 没有“lea,而是减去而不是加”如果这就是你的意思,你需要其他技巧
  • mov ebx,ecxsub ebx,edx 之前是最佳的。您可以通过许多其他方式实现相同的目标,例如neg edxlea ebx,[ecx+edx]neg edx,或在答案中使用push/pop,但所有这些变体都是人为复杂的,以避免最直接的变体并附带额外的性能损失。

标签: assembly x86 instructions


【解决方案1】:

使用 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 操作数整数指令,例如 ANDNSHRX

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 位为零)。

【讨论】:

  • 你总是可以... - 这有点夸大了。你不能用 MOV + SUB 做 x = y - x,尽管你可以做 neg eax / add eax, edx。仍然是 2 条指令,但它们都不是mov,因此 mov-elimination 不会将延迟降低到 1 个周期。 (就像在带有sub w0, w1, w0 的 AArch64 之类的三操作数机器上一样。)
【解决方案2】:

您始终可以使用堆栈来保存寄存器:

    push ecx
    push edx
    mov ecx, 1
    mov edx, 3
    sub ecx, edx
    mov ebx, ecx
    pop edx
    pop ecx

【讨论】:

  • 这给了我一个 ecx & edx 的随机数
  • @PandaAssassin :您的问题是您希望保持 ECX 和 EDX 与之前相同。此代码中的 ECX 将是 push ecx 之前的 ECX 中的任何内容。我认为您的问题可能是错误的?
  • @MichaelPetch 我现在明白了。我只是想将ecx 保留为 3。对我来说,我只会在 mov ecxedx 之后放置 push
  • 这是正确的,但过于复杂。我很想对此投反对票,因为大量的 push/pop 使您的代码变得混乱,难以阅读。
  • 我的意思是鼓励人们在不必要的时候使用 push/pop 会导致初学者代码无法阅读,这些代码会保存每个函数周围的所有寄存器,甚至是单个函数内部。我并不是说 2 是一船。顺便说一句,OP 的 C++ 源代码对 EDX 和 ECX 进行了修改,因此如果您必须在 push/pop 中使用 mov reg,imm,您不妨将其优化为 mov ebx, 1-3。 (为了记录,我不是这个答案的反对者。在我说我很想之后,其他人立即反对了。你可能认为那是我并且特别恼火。)
猜你喜欢
  • 1970-01-01
  • 2020-03-10
  • 2016-02-09
  • 2011-02-02
  • 2014-06-23
  • 2013-07-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多