
编程时,我们会将函数串联起来。函数 A 调用函数 B,依此类推。
你不必非得用这种方式编程,完全可以用一个函数编写整个程序。用一个函数编写一个非同寻常的程序会是一个很有趣的练习……前提是你把代码编写工作交给人工智能,因为人类很快就会被冗长的函数搞得焦头烂额。
编译器的一项关键优化是“内联”:编译器会获取你的函数定义,并尝试将其替换到调用位置。这在概念上非常简单。考虑以下示例,其中函数add3调用了函数add 。
int add ( int x , int y ) { return x + y ; } int add3 ( int x , int y , int z ) { return add ( add ( x , y ), z ); }您可以按如下方式手动内联调用。int add ( int x , int y ) { return x + y ; } int add3 ( int x , int y , int z ) { return add ( add ( x , y ), z ); }
int add3 ( int x , int y , int z ) { return x + y + z ; }函数调用在性能上相对便宜,但并非完全免费。如果函数需要一些非平凡的参数,你可能需要将它们保存到栈上并进行恢复,因此会产生额外的加载和存储操作。你需要跳转到函数内部,并在函数结束后跳转出来。此外,根据你系统的函数调用约定以及你使用的指令类型,函数调用的开头和结尾可能还会有额外的指令。int add3 ( int x , int y , int z ) { return x + y + z ; }
如果一个函数足够简单,例如我的add函数,那么在性能至关重要的情况下,它应该始终内联。让我们来看一个具体的例子。假设我要对一个数组中的整数求和。
for ( int x : numbers ) { sum = add ( sum , x ); }我使用的是MacBook(M4处理器,LLVM虚拟机)。for ( int x : numbers ) { sum = add ( sum , x ); }
| 功能 | ns/int |
|---|---|
| 常规的 | 0.7 |
| 排队 | 0.03 |
哇,内联版本速度快了20多倍。
让我们来看看发生了什么。’add’ 函数的调用点只是一个调用该函数的简单循环。
ldr w1 , [ x19 ], #0 x4 bl 0x100021740 ; add(int, int) cmp x19 , x20 b.ne 0x100001368 ; <+28>该函数本身非常简单:仅需两条指令。ldr w1 , [ x19 ], #0 x4 bl 0x100021740 ; add(int, int) cmp x19 , x20 b.ne 0x100001368 ; <+28>
add w0 , w1 , w0 ret因此,每次加法运算需要6条指令。每次加法运算大约需要3个循环。add w0 , w1 , w0 ret
内联函数呢?
ldp q4 , q5 , [ x12 , #-0 x20 ] ldp q6 , q7 , [ x12 ], #0 x40 add.4s v0 , v4 , v0 add.4s v1 , v5 , v1 add.4s v2 , v6 , v2 add.4s v3 , v7 , v3 subs x13 , x13 , #0 x10 b.ne 0x1000013fc ; <+104>这完全不同。编译器已将加法运算转换为高级(SIMD)指令集,使用 8 条指令处理 16 个整数块。因此,每个整数只需半条指令(之前需要 6 条指令)。也就是说,指令数量减少了 12 倍。除了指令数量减少之外,处理器每个周期还能执行更多指令,从而大幅提升性能。ldp q4 , q5 , [ x12 , #-0 x20 ] ldp q6 , q7 , [ x12 ], #0 x40 add.4s v0 , v4 , v0 add.4s v1 , v5 , v1 add.4s v2 , v6 , v2 add.4s v3 , v7 , v3 subs x13 , x13 , #0 x10 b.ne 0x1000013fc ; <+104>
如果我们阻止编译器使用这些高级指令,同时仍然进行内联,结果会怎样?我们仍然可以获得显著的性能提升(大约快 10 倍)。
| 功能 | ns/int |
|---|---|
| 常规的 | 0.7 |
| 排队 | 0.03 |
| 内联(无 SIMD) | 0.07 |
好的。但是add函数有点极端了。我们知道它应该始终内联。那么,像计算字符串中空格数量的函数这样不那么简单的函数呢?
size_t count_spaces ( std :: string_view sv ) { size_t count = 0 ; for ( char c : sv ) { if ( c == ' ' ) ++ count ; } return count ; }如果字符串足够长,那么函数调用的开销应该可以忽略不计。size_t count_spaces ( std :: string_view sv ) { size_t count = 0 ; for ( char c : sv ) { if ( c == ' ' ) ++ count ; } return count ; }
让我们传入一个长度为 1000 个字符的字符串。
| 功能 | ns/字符串 |
|---|---|
| 常规的 | 111 |
| 排队 | 115 |
内联版本不仅没有更快,反而略慢一些。我不确定原因。
如果我使用短字符串(比如 0 到 6 个字符)呢?那么内联函数的速度会明显更快。
| 功能 | ns/字符串 |
|---|---|
| 常规的 | 1.6 |
| 排队 | 1.0 |
要点:
- 如果性能是首要考虑因素,应尽可能使用内联函数,这样可以显著提高性能。
- 对于速度可能快也可能慢的函数,是否内联取决于输入数据。对于字符串处理函数,字符串的大小可能决定是否需要内联才能获得最佳性能。
注: 我的源代码是公开的。
原文: https://lemire.me/blog/2026/02/08/the-cost-of-a-function-call/