push 与 mov 的成本(堆栈与近内存)以及函数调用的开销

Posted

技术标签:

【中文标题】push 与 mov 的成本(堆栈与近内存)以及函数调用的开销【英文标题】:Cost of push vs. mov (stack vs. near memory), and the overhead of function calls 【发布时间】:2012-09-27 19:22:11 【问题描述】:

问题:

访问堆栈的速度是否与访问内存的速度相同?

例如,我可以选择在堆栈中做一些工作,或者我可以直接对内存中的标记位置进行工作。

所以,具体来说:push axmov [bx], ax 的速度相同吗?同样,pop axmov ax, [bx] 的速度相同吗? (假设 bx 在near 内存中拥有一个位置。)

提问的动机:

在 C 中通常不鼓励接受参数的琐碎函数。

我一直认为这是因为不仅参数必须在函数返回时被压入堆栈然后从堆栈中弹出,而且因为函数调用本身必须保留 CPU 的上下文,这意味着更多的堆栈使用.

但假设人们知道标题问题的答案,则应该可以根据等效数量的直接内存来量化函数用于设置自身(推送/弹出/保留上下文等)的开销访问。因此是标题问题。


编辑:澄清:上面使用的 near 与 16 位 x86 架构的 segmented memory model 中的 far 相对。)

【问题讨论】:

哇。我是探险家。我刚刚在 *** 上找到了一个很好的非 n00b 问题。用香槟和点赞庆祝我的探索! 我一直认为 ESP 上的 push/pop 调用的递减/递增操作与 mov.... 相比是一种开销。但我想应该还有更多。 【参考方案1】:

如今,您的 C 编译器可以胜过您。它可以内联简单的函数,如果这样做,将不会有函数调用或返回,并且可能不会有与传递和访问正式函数参数相关的额外堆栈操作(或当函数被内联但可用寄存器已用尽)如果一切都可以在寄存器中完成,或者更好的是,如果结果是一个常量值并且编译器可以看到并利用它。

函数调用本身在现代 CPU 上可能相对便宜(但不一定是零成本),如果它们是重复的,并且如果有单独的指令缓存和各种预测机制,有助于高效的代码执行。

除此之外,我希望选择“本地变量与全局变量”对性能的影响取决于内存使用模式。如果 CPU 中有内存缓存,则堆栈很可能在该缓存中,除非您在其上分配和解除分配大型数组或结构或进行深度函数调用或深度递归,从而导致缓存未命中。如果经常访问感兴趣的全局变量或者经常访问其邻居,我希望该变量大部分时间也在缓存中。同样,如果您正在访问无法放入缓存的大范围内存,您将遇到缓存未命中并可能降低性能(可能是因为可能有也可能没有更好的缓存友好的方式来执行您的操作想做)。

如果硬件相当笨拙(没有或很小的缓存、没有预测、没有指令重新排序、没有推测执行、什么都没有),显然你想减少内存压力和函数调用的数量,因为每个人都会算数。

还有一个因素是指令长度和解码。访问堆栈位置(相对于堆栈指针)的指令可以比访问给定地址的任意内存位置的指令短。较短的指令可以更快地解码和执行。

我想说对于所有情况都没有明确的答案,因为性能取决于:

您的硬件 你的编译器 您的程序及其内存访问模式

【讨论】:

以 x86 为例,16 位编译器通常会将所有参数压入堆栈,调用,创建堆栈帧,执行,移除堆栈帧并返回。对于 32 位和 64 位编译器,前几个参数在寄存器中传递,调用,堆栈帧可能会或可能不会创建/删除。编译器唯一需要遵守的是运行时环境:内部发生的事情取决于编译器的处理能力,有些编译器真的很棒。 @AKE:是的,mov [bx], ax 会比mov [0x1234], ax 短。 它可能会也可能不会更快。单独执行时可能是相同的(这很难做到,对吧?:),但根据周围的指令,它可能会更快。如果该单个字节足以导致缓存未命中怎么办? @AKE 你应该已经澄清了。有些人从未为 16 位 DOS 编程,也不知道近或远可能意味着什么。我决定不在这里做任何假设。使用段覆盖前缀可能会或可能不会受到惩罚。 CPU 在自身内部缓存了足够多的关于 cs,ds,es,fs,gs,ss 指向的段的段描述符的信息,因此,如果段寄存器已经加载了正确的值,则可能没有区别。 在 x86 近内存中,内存通过默认的段寄存器 cs 用于执行,ds 用于数据,ss 用于堆栈。一些指令还使用 es 作为默认寄存器。每当代码使用显式(从内存中获取数据!= 具有段寄存器覆盖的默认内存)或隐式段寄存器(远调用)访问时,您都在工作。总而言之,近是段内,远是段间。【参考方案2】:

对于时钟周期好奇...

对于希望查看特定时钟周期的用户,instruction / latency tables 可用于各种现代 x86 和 x86-64 CPU,here(感谢 hirschhornsalz 指出这些)。

然后你会在 Pentium 4 芯片上得到:

push axmov [bx], ax(红框)的效率几乎相同,延迟和吞吐量相同。 pop axmov ax, [bx](蓝框)同样高效,尽管 mov ax, [bx] 的延迟是 pop ax 的两倍,但吞吐量相同

至于 cmets 中的后续问题(第 3 条评论):

间接寻址(即mov [bx], ax)与直接寻址(即mov [loc], ax)没有本质区别,其中loc是一个保持立即值的变量,例如loc equ 0xfffd

结论:将其与Alexey's thorough answer 结合使用,有一个非常可靠的案例来提高使用堆栈的效率并让编译器决定何时内联函数。

(旁注:事实上,即使早在 1978 年的 8086,使用堆栈的效率仍然不低于相应的 mov 到内存,这可以从 these old 8086 instruction timing tables 看出。)


了解延迟和吞吐量

可能需要多一点了解现代 CPU 的时序表。这些应该会有所帮助:

definitions of latency and throughput a useful analogy 用于延迟和吞吐量,以及它们与指令处理管道的关系)

【讨论】:

以上是关于push 与 mov 的成本(堆栈与近内存)以及函数调用的开销的主要内容,如果未能解决你的问题,请参考以下文章

汇编程序从堆栈中写入字符

汇编 push ,pop指令

使用 'push' 或 'sub' x86 指令时如何分配堆栈内存?

堆栈溢出 - 静态内存与动态内存

计算机组成:解疑补漏之MOV指令与操作数寻址方式

在不需要清理时避免“push ebp, mov esp, ebp”序言