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,与操作系统追求实时性的设计目标冲突。协程的高效是以牺牲可抢占性为代价的。
相关工具
- perf stat:统计 CPU 指令数、缓存命中率
- strace:跟踪系统调用
- gdb disassemble:查看汇编指令
工程实践建议
- 对于 C/C++ 等底层语言,函数调用开销极低,可放心使用框架化设计。
- 对于 PHP/Java 等高级语言,单次调用开销虽高(几十 ns),但相对于毫秒级业务仍可忽略。
- 在 高并发 网络 IO 密集型场景中,优先使用异步非阻塞模型(如 Nginx)或协程(如 Go、Lua 协程),避免频繁的进程/线程切换。
- 协程虽轻量,但创建和调度仍有成本,避免不必要的
go操作。