为何空循环比调用空函数的循环运行速度更慢?
这事儿核心原因在于你用了-O0编译(无优化模式),编译器完全没做任何优化,导致两种循环的指令执行效率差异被放大了。咱们拆解来看:
1. 空循环的指令瓶颈:数据依赖导致流水线停滞
在-O0下,编译器为了方便调试,会把循环变量i存在栈内存里,而不是寄存器。空循环的汇编逻辑大概是这样:
loop_start: mov eax, [rbp-4] ; 从栈里读i的值 cmp eax, 1000000000 ; 和n比较 jge loop_end ; 满足条件就退出 add DWORD PTR [rbp-4], 1 ; i++,写回栈 jmp loop_start ; 回到循环开头 loop_end:
这里的问题是:每次循环都要先读i,加1后写回栈,下一次循环又要读这个刚写的值——这就形成了数据依赖:下一次的读操作必须等上一次的写操作完成才能执行。CPU的指令流水线会因为这个依赖被迫停顿,没法并行执行后续指令,导致每个循环的执行周期被拉长。而且空循环的指令太单一,CPU的超标量执行单元(能同时处理多条指令的单元)根本没活儿干,完全浪费了算力。
2. 调用空函数的循环:额外指令反而“盘活”了CPU
再看调用step()的循环,step()本身是空函数,它的汇编大概是:
step: push rbp mov rbp, rsp pop rbp ret
而循环的汇编会多一个call step的指令:
loop_start: mov eax, [rbp-4] cmp eax, 1000000000 jge loop_end call step ; 调用空函数 add DWORD PTR [rbp-4], 1 jmp loop_start loop_end:
虽然多了函数调用的开销,但这些push/mov/pop/ret指令之间几乎没有数据依赖,CPU的流水线可以并行处理这些指令。同时,在执行函数调用的这段时间里,CPU还能提前处理下一次循环的i读取操作——相当于把空循环里的停顿时间给填满了。而且这些额外指令让CPU的超标量单元有足够的工作可做,整体的指令执行吞吐量反而更高,最终总耗时更短。
补充:优化开启后情况会反转
如果你用-O2或更高优化级别编译,编译器会直接把空循环优化成“什么都不做”(因为循环体没有副作用),运行时间几乎为0;而调用step()的循环,编译器也可能会把step()内联,然后优化成空循环,但如果step()有某些无法内联的标记,才会保留函数调用。这时候空循环的速度肯定会比调用函数的循环快得多。
内容的提问来源于stack exchange,提问作者yxie

