3.6 函数调用与协程切换开销

10.1 函数CPU开销分析

C 语言函数调用开销

#include <stdio.h>
int func(int p) {
    return 1;
}
int main() {
    int i;
    for (i = 0; i < 100000000; i++) {
        func(2);
    }
    return 0;
}

耗时测试

# gcc main.c -o main
# time ./main
real    0m0.335s
user    0m0.334s
sys     0m0.000s
 
# perf stat ./main
......
1,100,989,673 instructions    # 1.37 insns per cycle
......

单独测试空循环(去掉 func()):

# time ./main
real    0m0.293s
user    0m0.292s
sys     0m0.000s
 
# perf stat ./main
......
301,252,997 instructions    # 0.43 insns per cycle
......

结论:C 语言一次函数调用耗时约为 0.4 ns(0.335 - 0.293) / 1e8),所需 CPU 指令数约为 8 条(1,100,989,673 - 301,252,997) / 1e8)。

指令级分析(使用 gdb disassemble 查看):

  • 调用前:mov $0x2, %edi(参数入寄存器),callq func
  • 函数内部:push %rbp, mov %rsp, %rbp, mov %edi, -0x4(%rbp), mov $0x1, %eax
  • 返回:leaveq, retq

大部分是寄存器操作,栈访问由 L1 缓存命中,延迟极低。

指令并行

现代 CPU 通过流水线可在一个周期内执行多条指令(如 1.37 insns per cycle),从而进一步降低了函数调用的实际开销。

PHP 语言函数调用开销

<?php
function func() {
    return true;
}
for ($i = 0; $i < 10000000; $i++) {
    func();
}
?>

测试结果

  • PHP 7:1000 万次调用耗时 0.667 s,减去空循环 0.140 s,平均每次 52 ns
  • PHP 5.3:1000 万次调用耗时 2.1 s,减去空循环 0.5 s,平均每次 160 ns

解释

PHP 在 C 之上又虚拟了一层指令集,每次函数调用需先解析 opcode,再转换为 CPU 指令,因此比 C 慢约两个数量级。但对于业务框架中成百上千次调用,50 μs 级的开销仍可接受。


10.2 协程切换CPU开销分析

协程切换耗时测试(Go 语言)

func cal() {
    for i := 0; i < 1000000; i++ {
        runtime.Gosched()
    }
}
func main() {
    runtime.GOMAXPROCS(1)
    currentTime := time.Now()
    fmt.Println(currentTime)
    go cal()
    for i := 0; i < 1000000; i++ {
        runtime.Gosched()
    }
    currentTime = time.Now()
    fmt.Println(currentTime)
}

运行结果

2019-08-08 22:35:13.415197171 +0800 CST
2019-08-08 22:35:13.655035993 +0800 CST

计算(655035993 - 415197171) ns / 2000000 ≈ 120 ns

注意

若包含协程创建(go 关键字),则单次创建+调度开销约为 400 ns,接近一次系统调用的耗时。不要滥用协程。

协程内存开销

  • 协程栈:默认初始大小 2 KB(Go 语言)
  • 线程栈:通常为 10 MB(可通过 ulimit -a 查看)

对比:100 万并发协程仅需约 2 GB 内存,而线程模型则需要约 10 TB。

协程 vs 进程/线程切换开销汇总

类型切换耗时栈内存
进程/线程上下文切换~3.5 μs~10 MB
协程切换(用户态)~120 ns~2 KB

为何操作系统不内置协程?

协程不可抢占,依赖于主动出让 CPU,与操作系统追求实时性的设计目标冲突。协程的高效是以牺牲可抢占性为代价的。

相关工具

工程实践建议

  • 对于 C/C++ 等底层语言,函数调用开销极低,可放心使用框架化设计。
  • 对于 PHP/Java 等高级语言,单次调用开销虽高(几十 ns),但相对于毫秒级业务仍可忽略。
  • 高并发 网络 IO 密集型场景中,优先使用异步非阻塞模型(如 Nginx)或协程(如 Go、Lua 协程),避免频繁的进程/线程切换。
  • 协程虽轻量,但创建和调度仍有成本,避免不必要的 go 操作。