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