使用 64 位 MASM 优化 C 函数调用

Optimizing a C function call using 64-bit MASM

目前正在使用此 64 位 MASM 代码调用 C 运行时函数,例如 memcmp(). I recall this convention was from a GoAsm article 优化。

              memcmp          PROTO;:QWORD,:QWORD,:QWORD
              PUSH            RSP
              PUSH            QWORD PTR [RSP]
              AND             SPL,0F0h
              MOV             R8,R11
              MOV             RDX,R10
              MOV             RCX,RAX
              SUB             RSP,32
              CALL            memcmp
              LEA             RSP,[RSP+40]
              POP             RSP

下面是有效的优化版本吗?

              memcmp          PROTO;:QWORD,:QWORD,:QWORD
              PUSH            RSP
              PUSH            QWORD PTR [RSP]
              AND             RSP,-16        ; new
              MOV             R8,R11
              MOV             RDX,R10
              MOV             RCX,RAX
              LEA             RSP,[RSP-32]   ; new
              CALL            memcmp
              LEA             RSP,[RSP+40]
              POP             RSP                  
              

替换的理由

              AND             SPL,0F0h
              

              AND             RSP,-16
              

是它避免调用部分寄存器更新。

正在替换

              SUB             RSP,32
              

              LEA             RSP,[RSP-32]

后续指令不依赖于减法更新的标志
那么不更新标志也会更有效率。
Why does GCC emit "lea" instead of "sub" for subtraction?

在这种情况下,是否还有其他优化技巧?

AND 是的,原始代码很愚蠢,没有保存任何代码大小(SPL 也采用 REX 前缀,就像 64 位 ope运行d-size)。

LEA - 毫无意义且浪费代码大小:x86 CPU 已经 ; that's necessary to efficiently run normal x86 code which is full of instructions like add, sub, and, etc. Compilers would use lea much more heavily if that wasn't the case. The answer on that linked Q&A is wrong and should be downvoted / deleted. The only danger is on a few less-common CPUs (Pentium 4 and Silvermont for different reasons) from instructions like inc that only write some flags. ()。即使是 Silvermont 系列上 inc 的成本也很小,只是一个额外的 uop 但在解码期间 不是 ,因此它不会停止。

add 在任何 CPU 上都不比 lea 慢,无论是本身还是对后续指令的影响。 (除了有序的 Atom pre-Silvermont,其中 lea 运行 在管道中比 add 更早(在实际的 AGU 上),因此它可能更好或更差,具体取决于数据的位置来自/去往)。您只会在某些情况下使用 lea,例如 adc 循环,您实际上需要保持 CF 不变,以便下一次迭代可以读取它。即不要弄乱真正的依赖关系(RAW),与避免错误的(WAW)输出依赖关系无关。 (请参阅 - note that cases where adc / inc / adc creates a partial-flag stalladd 会导致正确性问题的情况,因此我没有将其视为 add 会使后续指令更快的情况。)


您可能不需要保存旧的 RSP; ABI 在调用之前需要 16 字节堆栈对齐,这包括你的调用者(除非你从不遵循​​ ABI 的代码中调用,所以你不知道相对于 16 字节边界的 RSP 对齐).

通常你会像编译器那样做 sub rsp, 40,重新对齐 RSP 并为影子 space 保留 space。 (而且你会在函数的 top/bottom 处执行此操作,而不是在每次调用时,以及 saving/restoring 调用保留寄存器)。


(实际上 memcmp 不太可能关心堆栈对齐,除非它需要 save/restore 一些更多的 XMM regs。Windows x64 调用约定不明智 only has 6 call-clobbered x/ymm registers ,这可能会稍微紧一点,这取决于他们在手写的 (?) memcmp.)

中展开了多少循环

即使您确实需要处理未知的传入 RSP 对齐,将 RSP 保存到两个不同的位置以供 pop rsp 仍然不是一种非常有效的方法。通常,您只需使用 RBP 来制作一个传统的帧指针,以使用 mov rsp, rbp / pop rbp 进行清理,无论对 RSP 进行未知调整如何工作。例如即使在使用 alloca 的函数中(或在 asm 中,执行未知数量的推送或可变大小的 sub rsp,这实际上与 and rsp, -16 相同)。