如果我们使用
push ecx
我们应该在操作码中使用一个字节,如果我们使用了
sub esp, 4
我想我们应该用两个字节?我试着读过 文件 但我不太明白,原因和在《中国好声音》中一样。
xor eax, eax
而不是
mov eax, 0
TL:DR: Clang已经这样做了。 GCC没有,除了在 -Os
. 我没做过基准测试
代码大小并不是一切。 一个虚推仍然是一个真正的存储,它占用了一个存储缓冲区的条目,直到它提交到缓存。 事实上,代码大小通常是最不需要担心的事情,只有在所有其他因素都相同的情况下(前端uops的数量,避免后端瓶颈,避免任何性能陷阱)。
从历史上看(16位x86,CPU还没有缓存之前)。push cx
可能不会比 sub sp, 2
(3个字节)或 dec sp
dec sp
(2字节),在那些老的CPU上,内存带宽是影响性能的主要因素(包括取码)。 特别是在8088上优化速度与优化代码大小差不多。
原因是 xor eax,eax
仍然是首选的是,后来的CPU能够使它仍然至少一样快,即使除了代码大小的优势。 在x86汇编中,将一个寄存器置零的最好方法是什么:xor、mov还是and?
在后来的CPU上,比如PPro。push
解码为多个UOPS 来调整ESP,并分别进行存储)。 所以在这些CPU上,尽管代码尺寸较小,但在前端成本较高。 或者在P5奔腾上(没有把复杂的指令解码成多个uops)。push
暂时停滞了流水线,而且即使在希望获得存储到内存的副作用时,也经常被编译器所避免。
但最后,在Pentium-M前后。CPU有了 "堆栈引擎" ,处理了后端外的堆栈操作中的ESP-update部分,使得它的单uop和零延迟(对于通过ESP的dep链)。 从这个环节可以看出,堆栈引擎要插入的堆栈同步uop有时确实会使 sub esp,4
成本超过 push
如果你不是已经打算参考了 esp
直接在后端进行下一个栈操作之前。(就像 call
)
我不知道是否真的会有一个好主意,开始使用假人。push ecx
在那么老的CPU上,或者如果有限的存储缓冲区大小意味着把执行资源用在做虚拟存储上不是一个好主意,甚至用在几乎肯定是热的缓存行上(栈顶)。
但无论如何,现代编译器 做 利用这个窥视孔优化。 特别是在64位模式下,只需要通过一次推送调整堆栈是很常见的。 现代CPU的存储缓冲区很大。
void foo();
int bar() {
foo();
return 0;
}
Clang已经做了好几年了,比如用现在的clang 10.0 -O3(优化速度大于大小)。关于Godbolt
bar():
push rax
call foo()
xor eax, eax
pop rcx
ret
GCC在 -Os
但不是在 -O3
(我试过用 -march=skylake
但仍选择使用 sub
.)
要构建一个以下情况就不那么容易了,即 sub esp,4
将会很有用,但这个工作。
int bar() {
volatile int arr[1]= {0};
return 0;
}
clang10.0 -m32 -O3 -mtune=skylake
bar(): # @bar()
push eax
mov dword ptr [esp], 0 # missed optimization for push 0
xor eax, eax
pop ecx
ret
不幸的是,编译器没有发现这样一个事实,即 push 0
的初始化和保留空间。volatile int
对象,同时取代 push eax
和 mov dword [esp], 0
什么CC++编译器可以使用push pop指令来创建局部变量,而不是只增加一次esp?