为什么我们使用 sub esp, 4 而不是在汇编中压入一个寄存器?

Why do we use sub esp, 4 instead of push a register in assembly?

如果我们使用

push ecx

我们应该在操作码中使用一个字节,如果我们使用

sub esp, 4 

我想我们应该使用 2 个字节?我试过阅读 the documentation 但我不太了解。 原因同

xor eax, eax 

而不是

mov eax, 0

TL:DR: Clang 已经做到了。 GCC 不在 -Os 之外。我还没有进行基准测试。


代码大小不是一切。虚拟推送仍然是一个真正的存储,它占用一个存储缓冲区条目,直到它提交到缓存。 事实上,代码大小通常是最后一个需要担心的事情,只有当所有其他因素都相等时(数量前端 uops,避免后端瓶颈,避免任何性能陷阱)。

历史上(CPU 有缓存之前的 16 位 x86),push cx 可能不会比 sub sp, 2(3 字节)或 dec sp / dec sp 快(2 个字节)在那些内存带宽是性能主要因素(包括代码获取)的旧 CPU 上。优化 8088 的速度与优化代码大小大致相同。

xor eax,eax 仍然是首选的原因是后来的 CPU 能够使它至少保持同样快,即使除了代码大小的优势。


在后来的 CPU 上,如 PPro,push 解码为多个 uops(调整 ESP 并单独存储)。因此,在这些 CPU 上,尽管代码量较小,但在前端的成本更高。或者在 P5 Pentium(它没有将复杂指令解码为多个 uops)上,push 暂时停止了流水线,即使需要存储到内存的副作用,编译器也经常避免这样做。

但最终,围绕 Pentium-M, 在乱序后端之外处理堆栈操作的 ESP 更新部分,使其成为单 uop 和零延迟(对于通过 ESP 的 dep 链)。从 link 中可以看出,堆栈引擎必须插入的堆栈同步 uops 有时确实会使 sub esp,4 的成本超过 push,如果您还没有打算参考的话esp直接在后端下一个stack op之前。 (喜欢 call

IDK 如果在那么旧的 CPU 上开始使用虚拟 push ecx 真的是个好主意,或者如果有限的存储缓冲区大小意味着用完执行资源不是一个好主意在做虚拟存储时,甚至缓存几乎肯定是热的行(堆栈的顶部)。

但是无论如何,现代编译器使用这个窥孔优化,尤其是在需要调整堆栈的 64 位模式下一推很常见。现代 CPU 具有较大的存储缓冲区。

void foo();

int bar() {
    foo();
    return 0;
}

Clang 多年来一直这样做。例如当前 clang 10.0 -O3(优化速度超过大小)on 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 对象初始化和保留 space,替换 push eax 和 [=35] =]