注意:在宏展开后使用优化器来避免像 pop eax 这样的情况;push eax 所以不用担心!
在这种情况下,您应该尝试以您推送的寄存器中的结果结束,当您对堆栈顶部执行多项操作时,允许优化器链接寄存器操作而无需存储/重新加载。
Push 和 pop 是 1 字节指令,每个指令解码为 1 uop,现代 x86 CPU 中的堆栈引擎处理 ESP 的更新,避免了通过 ESP 的数据依赖链或额外的 uop 来添加/子 ESP。([esp]
但是,在寻址模式中显式使用会强制在 Intel CPU 上进行堆栈同步 uop,因此混合 push/pop 和直接访问堆栈可能会更糟。OTOH,add [esp], value
可以微融合加载+添加操作,其中单独的 pop/添加/推送不能。)
另请注意,以字节为单位最小化代码大小通常是保持关键路径延迟低的次要问题(对于已经将通过天真的 JIT 音译为机器代码存储/重新加载的堆栈架构很容易出现问题,而不是真正在堆栈操作之间进行优化,例如JVM会)。并且还可以最大限度地减少前端 uop 数量。在其他条件相同的情况下,较小的代码量通常是最好的。
并非所有指令都解码为单个 uop;例如add [mem], reg
解码为 2,inc [mem]
解码为 3 ,因为 load+inc 不能微融合在一起。大概不会有足够的指令级并行性来处理后端 uop,因此所有关于 uop 计数的讨论都是英特尔所谓的前端问题阶段的“融合域”,它将 uop 提供给 out-of-订单后端。
需要明确的是,这种简单的 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)
xor-zeroing避免了在写入 AL 后读取 EAX 可能导致的部分寄存器损失。请注意,我们只写入一次内存,而不是使用 memory-destination 再次存储然后重新加载+存储and
。 相同的优化适用<NOT-STACK-LOGICAL>
于<AND-STACK-LOGICAL>
总共 8 或 9 个字节(如果前导 pop 优化出来),总共 4 或 5 个 uop 加上 1 个堆栈同步 uop,所以 5 或 6 个。但是如果我们针对 push/pop 进行优化,则在 cmp+ 的微融合上给出 uop加载,有利于希望使用下一个函数优化推送/弹出对:
<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 微秒。8 到 10 个字节。能够优化尾随推送也可以在下一个块中保存一条指令,如果它发生的话。但更重要的是,避免了关键路径依赖链上的存储/重新加载延迟。
在它无法优化拖尾推动的情况下,情况几乎没有更糟,如果可以的话,情况会更好,所以这可能很好。
如果您可以op [esp]
根据是否可以优化推送/弹出来在弹出/操作/推送版本与版本之间进行选择,那么您可以选择两全其美。
<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 也选择在没有先对 EDX 进行异或归零的情况下编写 DL return a && b;
:https ://godbolt.org/z/hS9RrA 。但幸运的是,我们可以明智地选择并通过编写一个寄存器来减轻现代 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
(或优化推送/弹出消除,mov eax, [eax]
/ push eax
。)