push ebp
mov ebp, esp
sub esp, imm
有指令和内联函数这两种方法,它们之间有性能差异吗?如果有的话,哪种更快,为什么编译器总是使用后者?
leave
也是一样。
mov esp, ebp
pop ebp
说明。
push ebp
mov ebp, esp
sub esp, imm
有指令和内联函数这两种方法,它们之间有性能差异吗?如果有的话,哪种更快,为什么编译器总是使用后者?
leave
也是一样。
mov esp, ebp
pop ebp
说明。
性能方面有差异,尤其是enter
。在现代处理器上,这会解码为大约10到20个微操作,而三条指令序列大约为4到6条,具体取决于架构。有关详细信息,请参考Agner Fog的指令表。
此外,enter
指令通常具有相当高的延迟,例如在core2上为8个时钟周期,而三条指令序列的依赖链为3个时钟周期。
此外,编译器可能会根据周围代码的情况,将三条指令序列分散开来以进行调度,以允许更多指令的并行执行。
leave
又是怎么样的呢? - 小太郎leave
指令的性能相同,但据我所知,在任何情况下都不会更快。但它确实在指令高速缓存中占用较少的内存。 - Gunther Piezpush ebp
/ mov ebp, esp
/ sub esp, imm
指令并不存在3级循环依赖关系。中间的 mov
指令需要从 push
修改后的 ESP 中读取(在英特尔上需要花费一个栈同步 uop),但它本身不会修改 ESP。因此,sub esp, imm
指令可以与 mov
并行执行。自 Pentium-M 以来的堆栈引擎也意味着 push ebp
对于 ESP 并没有单独的延迟成本,它只是从 call
到达这段代码时添加到堆栈偏移量中的另一个堆栈偏移量罢了。 - Peter Cordes在设计80286时,英特尔的CPU设计师决定添加两条指令来帮助维护显示。
下面是CPU内部的微码:
; ENTER Locals, LexLevel
push bp ;Save dynamic link.
mov tempreg, sp ;Save for later.
cmp LexLevel, 0 ;Done if this is lex level zero.
je Lex0
lp:
dec LexLevel
jz Done ;Quit if at last lex level.
sub bp, 2 ;Index into display in prev act rec
push [bp] ; and push each element there.
jmp lp ;Repeat for each entry.
Done:
push tempreg ;Add entry for current lex level.
Lex0:
mov bp, tempreg ;Ptr to current act rec.
sub sp, Locals ;Allocate local storage
替代回车的方法是:
; enter n, 0 ; 在486上需要14个时钟周期
push bp ;1 cycle on the 486
sub sp, n ;1 cycle on the 486
输入n,1;在486处理器上需要17个周期。
push bp ;1 cycle on the 486
push [bp-2] ;4 cycles on the 486
mov bp, sp ;1 cycle on the 486
add bp, 2 ;1 cycle on the 486
sub sp, n ;1 cycle on the 486
输入n,3;在486上运行23个周期
push bp ;1 cycle on the 486
push [bp-2] ;4 cycles on the 486
push [bp-4] ;4 cycles on the 486
push [bp-6] ;4 cycles on the 486
mov bp, sp ;1 cycle on the 486
add bp, 6 ;1 cycle on the 486
sub sp, n ;1 cycle on the 486
等等,长方法可能会增加文件大小,但速度更快。
最后一个注意点是,程序员现在不再使用display,因为那是一种非常慢的解决方法,使得ENTER变得相当无用。
来源:https://courses.engr.illinois.edu/ece390/books/artofasm/CH12/CH12-3.html
enter
在所有CPU上的速度都特别慢,除了可能用于代码大小优化来换取速度之外,没有人使用它。(如果需要帧指针,或者希望允许更紧凑的寻址模式以寻址堆栈空间。)
leave
足够快,值得使用,GCC也使用它(如果ESP/RSP没有已经指向保存的EBP/RBP,则使用它; 否则,它只使用pop ebp
)。leave
仅为3个微操作,某些AMD上为2个。(https://agner.org/optimize/,https://uops.info/)。leave
比分开执行操作仅多了一个微操作。我曾在Skylake上测试过这一点,在循环中比较了调用/返回和使用mov
/pop
或leave
设置传统帧指针并拆除其堆栈帧的函数。对于uops_issued.any
的perf
计数器,使用leave
时会多出一个前端微操作,而mov/pop则没有。(我运行了自己的测试,以防其他的测量方法在其leave
测量中计算了一个堆栈同步微操作,但在实际函数中使用它可以控制这一点。)在大多数没有微操作缓存的CPU中(即Sandybridge之前的英特尔CPU,Zen之前的AMD CPU),多微操作指令可能成为解码瓶颈。它们只能在第一个(“复杂”)解码器中解码,因此可能意味着比正常情况下产生的微操作要少的解码周期。
有些Windows调用约定是被叫方弹出堆栈参数,使用ret n
。(例如,在弹出返回地址后,ret 8
会使ESP/RSP += 8)。这是一个多微操作指令,与现代x86上的普通近距离ret
不同。所以上述原因是双倍的:leave和ret 12
不能在同一个周期内解码。
这些原因也适用于构建微操作缓存条目的遗留解码。
P5 Pentium更喜欢x86的类似RISC的子集,甚至无法将复杂指令拆分为单独的微操作。
LEAVE
的优点(仍然在使用中,只需查看Windows dlls)是比手动拆卸堆栈帧更小,这在空间有限时非常有帮助。ENTER
vs PUSH/MOV
,发现它们之间的差异大约是 2 倍。因此,如果您有一个循环超过一百万次的 CALL
,它会开始稍微有所不同。我尝试了一亿次迭代,大约需要 1 秒钟。因此,如果您在调用中有 CALL
,它会使延迟倍增... 我认为对于编译器来说,这绝对是一个很好的优化。 - Alexis Wilke