如果已经分配了足够的空间(例如,像您建议的那样在函数中使用较早的空间),编译器通常会选择mov
存储 args 而不是。push
sub esp, 0x10
这是一个例子:
int f1(int);
int f2(int,int);
int foo(int a) {
f1(2);
f2(3,4);
return f1(a);
}
由clang6.0 -O3 -march=haswell
on Godbolt编译
sub esp, 12 # reserve space to realign stack by 16
mov dword ptr [esp], 2 # store arg
call f1(int)
# reuse the same arg-passing space for the next function
mov dword ptr [esp + 4], 4
mov dword ptr [esp], 3
call f2(int, int)
add esp, 12
# now ESP is pointing to our own arg
jmp f1(int) # TAILCALL
sub esp,8
使用/时clang 的 code-gen 会更好push 2
,但函数的其余部分保持不变。即让push
堆栈增长,因为它具有更小的代码大小mov
,尤其是mov
-immediate,并且性能并不差(因为我们即将call
使用堆栈引擎)。请参阅哪些 C/C++ 编译器可以使用 push pop 指令来创建局部变量,而不仅仅是增加 esp 一次?更多细节。
我还在 Godbolt 链接 GCC 输出中包含/不-maccumulate-outgoing-args
包含延迟清除堆栈直到函数结束。.
默认情况下(不累积传出参数)gcc 确实让 ESP 反弹,甚至使用 2xpop
从堆栈中清除 2 个参数。(避免堆栈同步 uop,代价是在 L1d 缓存中命中 2 个无用负载)。需要清除 3 个或更多参数时,gcc 使用add esp, 4*N
. 我怀疑通过mov
存储重用 arg 传递空间而不是 add esp / push 有时会提高整体性能,尤其是使用寄存器而不是立即数。(比 .push imm8
紧凑得多mov imm32
。)
foo(int): # gcc7.3 -O3 -m32 output
push ebx
sub esp, 20
mov ebx, DWORD PTR [esp+28] # load the arg even though we never need it in a register
push 2 # first function arg
call f1(int)
pop eax
pop edx # clear the stack
push 4
push 3 # and write the next two args
call f2(int, int)
mov DWORD PTR [esp+32], ebx # store `a` back where we it already was
add esp, 24
pop ebx
jmp f1(int) # and tailcall
使用-maccumulate-outgoing-args
,输出基本上就像 clang,但 gcc在执行尾调用之前仍然保存/恢复ebx
并保留在其中。a
请注意,让 ESP 反弹需要额外的元数据来.eh_frame
进行堆栈展开。 Jan Hubicka 在 2014 年写道:
arg 积累还是有利有弊的。我对 AMD 芯片进行了相当广泛的测试,发现它的性能是中性的。在 32 位代码上,它节省了大约 4% 的代码,但在禁用帧指针的情况下,它扩展了很多展开信息,因此生成的二进制文件大约大 8%。(这也是 的当前默认值-Os
)
因此,使用 push for args 并至少通常在每次call
. 我认为这里有一个快乐的媒介,gcc 可以在不push
使用/的情况下使用更多。 push
pop
之前维护 16 字节堆栈对齐会产生混淆效果call
,这是当前版本的 i386 System V ABI 所要求的。在 32 位模式下,它曾经只是 gcc 默认维护的-mpreferred-stack-boundary=4
. (即 1<<4)。我认为您仍然可以使用
-mpreferred-stack-boundary=2
违反 ABI 并制作只关心 ESP 的 4B 对齐的代码。
我没有在 Godbolt 上尝试过,但你可以。