C++ 什么样的信用证+;编译器可以使用push-pop指令来创建局部变量,而不是只增加一次esp?

C++ 什么样的信用证+;编译器可以使用push-pop指令来创建局部变量,而不是只增加一次esp?,c++,assembly,x86,compiler-optimization,micro-optimization,C++,Assembly,X86,Compiler Optimization,Micro Optimization,我相信push/pop指令将产生更紧凑的代码,甚至可能会运行得稍微快一点。但这也需要禁用堆栈帧 要检查这一点,我需要手工重写一个足够大的汇编程序(比较它们),或者安装和研究一些其他编译器(看看它们是否有这样的选项,并比较结果) 下面是关于这个和类似问题的讨论 简而言之,我想了解哪种代码更好。代码如下: sub esp, c mov [esp+8],eax mov [esp+4],ecx mov [esp],edx ... add esp, c push eax push ecx push ed

我相信push/pop指令将产生更紧凑的代码,甚至可能会运行得稍微快一点。但这也需要禁用堆栈帧

要检查这一点,我需要手工重写一个足够大的汇编程序(比较它们),或者安装和研究一些其他编译器(看看它们是否有这样的选项,并比较结果)

下面是关于这个和类似问题的讨论

简而言之,我想了解哪种代码更好。代码如下:

sub esp, c
mov [esp+8],eax
mov [esp+4],ecx
mov [esp],edx
...
add esp, c
push eax
push ecx
push edx
...
add esp, c
longarr_arg_handtuned:
    push    rdx
    push    rsi
    push    rdi                 # leave stack 16B-aligned
    mov     rsp, rdi
    call    ext_longarr(long*)
    add     rsp, 24
    ret
或者像这样的代码:

sub esp, c
mov [esp+8],eax
mov [esp+4],ecx
mov [esp],edx
...
add esp, c
push eax
push ecx
push edx
...
add esp, c
longarr_arg_handtuned:
    push    rdx
    push    rsi
    push    rdi                 # leave stack 16B-aligned
    mov     rsp, rdi
    call    ext_longarr(long*)
    add     rsp, 24
    ret

哪种编译器可以生成第二种代码?它们通常会产生第一个编译器的一些变体。

你说得对,
推送
对于所有4个主要x86编译器来说都是一个小的遗漏优化。有一些代码大小,因此间接地要有性能。或者在某些情况下更直接地使用少量性能,例如保存
子rsp
指令

但如果不小心,可以通过将
推送
[rsp+x]
寻址模式混合使用额外的堆栈同步UOP来降低速度
pop
听起来没什么用,只是
push
。正如建议的那样,你只在最初储存本地人时使用它;以后的重新加载和存储应使用正常寻址模式,如
[rsp+8]
。我们不是说试图完全避免
mov
加载/存储,我们仍然希望随机访问从寄存器溢出局部变量的堆栈插槽


现代代码生成器避免使用PUSH。它在今天的处理器上效率很低,因为它修改了堆栈指针,这会使超级标量内核变得一团糟

这在15年前是正确的,但编译器在优化速度而不仅仅是代码大小时再次使用了
push
编译器已经使用
push
/
pop
来保存/恢复它们想要使用的调用保留寄存器,如
rbx
,并用于推送堆栈参数(主要在32位模式下;在64位模式下,大多数参数适合寄存器)。这两件事都可以用
mov
来完成,但是编译器使用
push
,因为它比
sub-rsp,8
/
mov[rsp],rbx
更有效gcc有一些调优选项,可以避免在这些情况下推送
/
pop
,为
-mtune=pentium3
-mtune=pentium
以及类似的旧CPU启用,但不适用于现代CPU。

对于PUSH/POP/CALL/RET,它跟踪对RSP的更改,无延迟且无ALU UOP。许多实际代码仍在使用PUSH/POP,因此CPU设计者添加了硬件以提高效率。现在我们可以(小心地!)在调整性能时使用它们。请参阅,以及他的asm优化手册。他们很棒。(和中的其他链接。)

它并不完美;直接读取RSP(当与无序内核中的值的偏移量为非零时)确实会导致在Intel CPU上插入堆栈同步uop。e、 g.
push-rax
/
mov[rsp-8],rdi
总共是3个融合域UOP:2个存储和一个堆栈同步

在函数输入时,“堆栈引擎”已经处于非零偏移状态(来自父函数中的
调用
),因此在第一次直接引用RSP之前使用一些
推送
指令根本不需要额外的UOP。(除非我们是从另一个带有
jmp
的函数调用的,并且该函数在
jmp
之前没有
pop
任何东西)

有一段时间有点可笑,因为它是如此便宜和紧凑(如果你只做一次,而不是10次分配80个字节),但却没有利用它来存储有用的数据。堆栈在缓存中几乎总是热的,现代CPU对L1d具有非常出色的存储/加载带宽


使用
clang6.0-O3-march=haswell编译
查看该链接了解所有其他代码,以及许多不同的遗漏优化和愚蠢的代码gen(请参阅我在C源代码中的注释,指出其中一些):

与gcc、ICC和MSVC非常相似的代码,有时指令顺序不同,或者gcc会毫无理由地保留额外的16B堆栈空间。(MSVC保留了更多的空间,因为它针对的是Windows x64调用约定,该约定保留了阴影空间,而不是红色区域)

clang通过使用存储地址的LEA结果而不是重复RSP相对地址(SIB+disp8)来节省代码大小。ICC和clang将变量放在其保留空间的底部,因此其中一种寻址模式避免了
disp8
。(对于3个变量,保留24字节而不是8字节是必要的,而clang当时并没有利用这一点。)gcc和MSVC错过了这一优化

但无论如何,更理想的方法是

    push    2                       # only 2 bytes
    lea     rdi, [rsp + 4]
    mov     dword ptr [rdi], 1
    mov     rsi, rsp                # special case for lea rsi, [rsp + 0]
    call    extfunc(int*, int*)
      # ... later accesses would use [rsp] and [rsp+] if needed, not pop
    pop     rax                     # alternative to add rsp,8
    ret
push
是一个8字节的存储,我们重叠了其中的一半。这不是问题,即使在存储了高半部之后,CPU也可以有效地向前存储未修改的低半部。重叠存储通常不是问题,事实上,对于小拷贝(至少达到2x xmm寄存器的大小),使用两个(可能)重叠加载+存储来加载所有内容,然后存储所有内容,而不考虑是否存在重叠

请注意,在64位模式下。因此,我们仍然必须直接引用qword的上半部分的
rsp
。但是如果我们的变量是uint64\t,或者我们不关心使它们相邻,那么我们可以使用
push

在这种情况下,我们必须显式引用RSP,以获取指向局部变量的指针,以便传递到另一个函数,因此无法绕过Intel CPU上的额外堆栈同步uop。在其他情况下,您可能只需要溢出一些函数参数f
longarr_arg(long, long, long):                     # @longarr_arg(long, long, long)
    sub     rsp, 24
    mov     rax, rsp                 # this is clang being silly
    mov     qword ptr [rax], rdi     # it could have used [rsp] for the first store at least,
    mov     qword ptr [rax + 8], rsi   # so it didn't need 2 reg,reg MOVs to avoid clobbering RDI before storing it.
    mov     qword ptr [rax + 16], rdx
    mov     rdi, rax
    call    ext_longarr(long*)
    add     rsp, 24
    ret
longarr_arg_handtuned:
    push    rdx
    push    rsi
    push    rdi                 # leave stack 16B-aligned
    mov     rsp, rdi
    call    ext_longarr(long*)
    add     rsp, 24
    ret
save_slice_farpointer:
[...]
.main:
[...]
    lframe near
    lpar word,  segment
    lpar word,  offset
    lpar word,  index
    lenter
    lvar word,  orig_cx
     push cx
    mov cx, SYMMAIN_index_size
    lvar word,  index_size
     push cx
    lvar dword, start_pointer
     push word [sym_storage.main.start + 2]
     push word [sym_storage.main.start]