当您有任何内存输入/输出时,通常应避免在 inline-asm 中修改 ESP,因此您不必禁用优化或强制编译器以其他方式使用 EBP 创建堆栈帧。一个主要优点是您(或编译器)可以使用 EBP 作为额外的免费寄存器;如果您已经不得不溢出/重新加载东西,则可能会显着加速。如果您正在编写内联汇编,大概这是一个热点,因此值得花费额外的代码大小来使用 ESP 相对寻址模式。
在 x86-64 代码中,安全使用 push/pop 存在额外的障碍,因为 you can't tell the compiler you want to clobber the red-zone 低于 RSP。 (您可以使用-mno-red-zone 进行编译,但无法从C 源代码中禁用它。)您可能会遇到问题like this,因为您在堆栈上破坏了编译器的数据。但是,没有 32 位 x86 ABI 具有红色区域,因此这只适用于 x86-64 System V。(或具有红色区域的非 x86 ISA。)
如果您想将 push 之类的纯 asm 操作作为堆栈数据结构,则您只需要 -fno-omit-frame-pointer 用于该功能,因此推送量是可变的。或者,如果针对代码大小进行优化。
您总是可以在 asm 中编写一个完整的非内联函数并将其放在一个单独的文件中,然后您就可以完全控制。但只有在你的函数足够大以至于值得调用/调用开销时才这样做,例如如果它包括一个完整的循环;不要让编译器 call 在 C 内部循环中成为一个短的非循环函数,破坏所有调用破坏的寄存器并且必须确保全局变量是同步的。
您似乎在 inline asm 中使用 push / pop,因为您没有足够的寄存器,需要保存/重新加载某些内容。 您不需要使用 push/pop 来保存/恢复。相反,使用带有"=m" 约束的虚拟输出操作数让编译器为您分配堆栈空间,并使用mov 到/从这些插槽。 (当然,您不仅限于mov;如果您只需要一次或两次值,那么将内存源操作数用于 ALU 指令可能是一种胜利。)
这对于代码大小可能会稍差一些,但对于性能通常不会更差(并且可能会更好)。如果这还不够好,请在 asm 中编写整个函数(或整个循环),这样您就不必与编译器搏斗。
int foo(char *p, int a, int b) {
int t1,t2; // dummy output spill slots
int r1,r2; // dummy output tmp registers
int res;
asm ("# operands: %0 %1 %2 %3 %4 %5 %6 %7 %8\n\t"
"imull $123, %[b], %[res]\n\t"
"mov %[res], %[spill1]\n\t"
"mov %[a], %%ecx\n\t"
"mov %[b], %[tmp1]\n\t" // let the compiler allocate tmp regs, unless you need specific regs e.g. for a shift count
"mov %[spill1], %[res]\n\t"
: [res] "=&r" (res),
[tmp1] "=&r" (r1), [tmp2] "=&r" (r2), // early-clobber
[spill1] "=m" (t1), [spill2] "=&rm" (t2) // allow spilling to a register if there are spare regs
, [p] "+&r" (p)
, "+m" (*(char (*)[]) p) // dummy in/output instead of memory clobber
: [a] "rmi" (a), [b] "rm" (b) // a can be an immediate, but b can't
: "ecx"
);
return res;
// p unused in the rest of the function
// so it's really just an input to the asm,
// which the asm is allowed to destroy
}
这将编译为带有 gcc7.3 -O3 -m32 on the Godbolt compiler explorer 的以下 asm。注意 asm 注释显示了编译器为所有模板操作数选择的内容:它选择了 12(%esp) 为 %[spill1] 和 %edi 为 %[spill2] (因为我使用 "=&rm" 为该操作数,所以编译器保存/恢复%edi 在 asm 之外,并将它交给我们作为那个虚拟操作数)。
foo(char*, int, int):
pushl %ebp
pushl %edi
pushl %esi
pushl %ebx
subl $16, %esp
movl 36(%esp), %edx
movl %edx, %ebp
#APP
# 19 "/tmp/compiler-explorer-compiler118120-55-w92ge8.v797i/example.cpp" 1
# operands: %eax %ebx %esi 12(%esp) %edi %ebp (%edx) 40(%esp) 44(%esp)
imull $123, 44(%esp), %eax
mov %eax, 12(%esp)
mov 40(%esp), %ecx
mov 44(%esp), %ebx
mov 12(%esp), %eax
# 0 "" 2
#NO_APP
addl $16, %esp
popl %ebx
popl %esi
popl %edi
popl %ebp
ret
嗯,告诉编译器我们修改哪个内存的虚拟内存操作数似乎导致专用一个寄存器,我猜是因为p 操作数是早期破坏,所以它不能使用相同的寄存器。如果您确信其他输入都不会使用与p 相同的寄存器,我想您可能会冒着放弃早期破坏的风险。 (即它们没有相同的值)。