我对计算机科学和软件工程充满热情,尤其是低级编程。 软件和硬件之间的相互作用是无穷无尽的,为调试高级应用程序提供了宝贵的见解。 一个典型的例子是堆栈内存;了解其机制对于高效的代码和有效的故障排除至关重要。
本文通过检查函数调用产生的开销来探讨频繁的函数调用对性能的影响。 假设您对堆栈和堆内存以及 CPU 寄存器有基本的了解。
理解堆栈帧
考虑一个程序的执行。操作系统为程序分配内存,包括堆栈。 每个线程的典型最大堆栈大小为 8 MB(可在 Linux/Unix 上使用 ulimit -s
进行验证)。 堆栈存储函数参数、局部变量和执行上下文。它相对于堆内存的速度优势源于操作系统预分配;分配不需要不断的操作系统调用。与用于较大的持久数据的堆内存不同,这使其成为小型临时数据的理想选择。
多个函数调用会导致上下文切换。例如:
<code class="language-c">#include <stdio.h> int sum(int a, int b) { return a + b; } int main() { int a = 1, b = 3; int result; result = sum(a, b); printf("%d\n", result); return 0; }</code>
调用sum
需要CPU:
main
)。sum
。这些保存的数据构成了堆栈帧。 每个函数调用都会创建一个新框架;函数完成逆转了这个过程。
性能影响
函数调用本质上会带来开销。这在频繁调用的循环或深度递归等场景中变得很重要。
C 提供了在性能关键型应用程序(例如嵌入式系统或游戏开发)中缓解这种情况的技术。 宏或 inline
关键字可以减少开销:
<code class="language-c">static inline int sum(int a, int b) { return a + b; }</code>
或
<code class="language-c">#define SUM(a, b) ((a) + (b))</code>
虽然两者都避免创建堆栈帧,但由于类型安全性,内联函数是首选,这与可能引入微妙错误的宏不同。 现代编译器通常会自动内联函数(使用 -O2
或 -O3
等优化标志),除了在特定上下文中之外,通常不需要显式使用。
装配级考试
分析汇编代码(使用objdump
或gdb
)揭示堆栈帧管理:
<code class="language-assembly">0000000000001149 <sum>: 1149: f3 0f 1e fa endbr64 # Indirect branch protection (may vary by system) 114d: 55 push %rbp # Save base pointer 114e: 48 89 e5 mov %rsp,%rbp # Set new base pointer 1151: 89 7d fc mov %edi,-0x4(%rbp) # Save first argument (a) on the stack 1154: 89 75 f8 mov %esi,-0x8(%rbp) # Save second argument (b) on the stack 1157: 8b 55 fc mov -0x4(%rbp),%edx # Load first argument (a) from the stack 115a: 8b 45 f8 mov -0x8(%rbp),%eax # Load second argument (b) from the stack 115d: 01 d0 add %edx,%eax # Add the two arguments 115f: 5d pop %rbp # Restore base pointer 1160: c3 ret # Return to the caller </sum></code>
push
、mov
和 pop
指令管理堆栈帧,突出显示开销。
当优化至关重要时
虽然现代 CPU 可以有效地处理这种开销,但它在嵌入式系统或高要求应用程序等资源受限的环境中仍然具有相关性。 在这些情况下,最小化函数调用开销可以显着提高性能并减少延迟。 然而,优先考虑代码可读性仍然是最重要的;应明智地应用这些优化。
以上是堆栈帧和函数调用:它们如何产生 CPU 开销的详细内容。更多信息请关注PHP中文网其他相关文章!