这些堆栈操作是最小的 x86 宏吗?

Are these the smallest possible x86 macros for these stack operations?

我正在将基于堆栈的语言作为一个有趣的个人项目。因此,我在堆栈上有一些 signed/unsigned 32 位值,我的目标是编写一些在此堆栈上运行的汇编宏。理想情况下,它们会很小,因为它们会被大量使用。由于我是 x86 汇编的新手,我想知道你们是否有任何可以想到的提示或改进。非常感谢您抽出时间,谢谢!

注意:宏展开后会使用优化器来避免像pop eax; push eax这样的情况,所以不用担心!

<SIGNED/UNSIGNED-COMPARISONS>
pop eax
cmp dword [esp], eax
setl byte [esp]        ;setle, setg, setge, setb, setbe, seta, setae, sete, setne for other comparisons
and dword [esp], 1

<NEGATE-STACK>
neg dword [esp]

<NOT-STACK>
not dword [esp]

<DEREF-STACK>
mov eax, dword [esp]      ;eax = address
mov eax, dword [eax]      ;eax = *address 
mov dword [esp], eax      ;stack = eax

<SHIFT>
pop ecx
SHL dword [esp], cl  ;SAR, SHR

<ADD-STACK>
pop eax
add dword [esp], eax    ;sub, and, or, xor

<AND-STACK-LOGICAL>
cmp dword [esp], 0
setne byte [esp]
and dword [esp], 1
comp dword [esp+4], 0
setne byte [esp+4]
and dword [esp+4], 1
pop eax
and dword [esp], eax    ;or

<NOT-STACK-LOGICAL>
cmp dword [esp], 0
sete byte [esp]
and dword [esp], 1

<MULTIPLY-STACK>
pop eax
imul eax, dword [esp]     ;imul works for signed and unsigned
mov dword [esp], eax

<DIVIDE-UNSIGNED-STACK>
pop ebx
pop eax
mov edx, 0
div ebx
push eax    ;edx for modulo

<DIVIDE-SIGNED-STACK>
pop ebx
pop eax
cdq
idiv ebx
push eax    ;edx for modulo

Note: An optimizer is used after the macros are expanded to avoid cases like pop eax; push eax so don't worry about that!

在那种情况下,你应该尝试以你推送的寄存器中的结果结束,允许优化器在你执行多个操作时无需store/reload链接寄存器操作事情到堆栈的顶部。

Push 和 pop 是 1 字节指令,每个指令解码为 1 uop, 处理 ESP 的更新,避免通过 ESP 的数据依赖链或额外的 uop 到 add/sub ESP。 (不过,在寻址模式下显式使用 [esp] 会在 Intel CPU 上强制执行堆栈同步 uop,因此混合 push/pop 和直接访问堆栈可能会更糟。OTOH,add [esp], value 可以微- 融合加载+添加操作,其中单独的 pop/add/push 不能。)

另请注意,以字节为单位最小化代码大小通常次要于保持低关键路径延迟(对于已经将使用天真的 JIT 音译存储/重新加载到机器代码的堆栈架构来说,这很容易成为问题,而不是真正的像 JVM 那样在堆栈操作之间进行优化)。并且还可以最大程度地减少前端 uop 数量。不过,在其他条件相同的情况下,较小的代码通常是最好的。

并非所有指令都解码为单个 uop;例如add [mem], reg 解码为 2,inc [mem] 解码为 3 。大概没有足够的指令级并行性让后端 uops 变得重要,所以所有关于 uop 计数的讨论都是英特尔所说的前端问题阶段的“融合域”,它将 uops 馈送到外部 -订单后端。

需要说明的是,这种简单的固定序列 JIT 之间只有很小的优化可能适用于玩具项目,但真正的 JIT 编译器适用于基于堆栈的字节码,如 Java 优化 更多,在每个函数的基础上进行寄存器分配。 如果您真正想要好的性能,这种方法是死胡同。如果您只是想通过业余项目学习一些 asm 和一些编译器构造,这看起来可能没问题。您可能需要其他操作,包括交换前两个元素 (pop/pop / push/push).


您的比较代码很糟糕,当 and 执行与最近指令中的字节存储重叠的双字加载时,会造成存储转发停顿。 (https://agner.org/optimize/)。使用像 ECX 或 EDX 这样的 tmp 寄存器。

<SIGNED/UNSIGNED-COMPARISONS>  version 1
pop   ecx                ; 1 byte, 1 uop, maybe optimized out
xor   eax,eax            ; 2 bytes, 1 uop
cmp   dword [esp], ecx   ; 3 bytes (ESP addressing mode needs a SIB), 1 uop (micro-fused load+cmp)
setl  al          ; 3 bytes, 0F ... 2-byte opcode ;setle, setg, setge, setb, setbe, seta, setae, sete, setne for other comparisons
mov   [esp], eax         ; 3 bytes (ESP addr mode), 1 uop (micro-fused store-address + store-data)

avoids a possible 用于在写入 AL 后读取 EAX。请注意,我们只写入一次内存,而不是存储,然后使用内存目标 and 重新加载+存储。 相同的优化适用于 <NOT-STACK-LOGICAL>,并且在 <AND-STACK-LOGICAL>

中有所应用

总共 8 或 9 个字节(如果领先的弹出优化),总共 4 或 5 微指令加上 1 个堆栈同步微指令,所以 5 或 6。但是如果我们优化 push/pop,给出微指令关于 cmp+load 的微融合,有利于希望用下一个函数优化掉 push/pop 对:

<SIGNED/UNSIGNED-COMPARISONS>  version 2
pop    ecx                          ; 1 byte, 1 uop, hopefully optimized out
xor    eax,eax       ; EAX=0        ; 2 bytes, 1 uop
pop    edx                          ; 1 byte, 1 uop
cmp    edx, ecx                     ; 2 bytes, 1 uop
setl   al          ; 3 bytes, 1 uop ;setle, setg, setge, setb, setbe, seta, setae, sete, setne for other comparisons
push   eax                          ; 1 byte, 1 uop, hopefully optimized out
  ;; no references to [esp] needing a stack-sync uop

4 到 6 微指令,如果前导弹出 and/or 尾随推送优化。 8 到 10 个字节。能够优化尾随推送也可以在下一个块中保存一条指令(如果发生的话)。但更重要的是,避免了关键路径依赖链上的 store/reload 延迟。

在无法优化尾随推送的情况下,情况几乎没有更糟,如果可以则更好,所以这可能很好。

如果你可以 select 在 pop/operate/push 版本和 op [esp] 版本之间根据是否可以优化掉 push/pop ,那会让你选择最好的两个世界。


<DIVIDE-UNSIGNED-STACK> 这里没有理由使用 EBX;你可以使用 ECX,就像你已经在为轮班而烦恼一样,让你有可能在 EBX 中保留一些有用的东西。您还可以对 EDX 使用异或归零。出于某种原因,您决定在 div 之前弹出两个操作数,而不是 div dword [esp]。没关系,如上所述 push/pop 有望优化掉。

<DIVIDE-UNSIGNED-STACK>
pop ebx
pop eax
mov edx, 0
div ebx
push eax    ;edx for modulo

您的 AND-STACK-LOGICAL 应该在寄存器而不是内存中计算。获得完整的双字宽度结果并避免错误的依赖性/部分寄存器恶作剧是很烦人的。例如,即使是 GCC 也选择在不首先对 return a && b;: https://godbolt.org/z/hS9RrA 的 EDX 进行异或归零的情况下编写 DL。但幸运的是,我们可以通过编写一个已经是导致该指令的依赖链的一部分的寄存器来明智地选择并减轻现代 CPU 的错误依赖。 (and eax,edx 在写入 dl 后仍会导致 Nehalem / Core2 或更早版本的部分寄存器停顿,但这些已过时。)

<AND-STACK-LOGICAL>
pop   ecx
pop   edx
xor   eax, eax
test  ecx, ecx         ; peephole optimization for cmp reg,0  when you have a value in a reg
setnz al               ; same instruction (opcode) as setne, but the semantic meaning for humans is better for NZ then NE
test  edx, edx
setnz dl
and   eax, edx         ; high garbage in EDX above DL ANDs away to zero.
push  eax
; net 1 total pop = 2 pop + 1 push

这将比您的 AND-STACK-LOGICAL 更小但更好,从而避免多个存储转发停顿。


<DEREF-STACK> 也可以使用一些工作,正如 Nate 指出的那样:

<DEREF-STACK>
pop eax                   ;eax = address
push dword [eax]          ;push *address 

(或针对 push/pop 消除进行优化,mov eax, [eax] / push eax。)