为何O3优化下sub与imul指令重排可提升性能?
编译优化中指令重排的疑问与解答
示例代码(来自《Computer Systems: A Programmer's Perspective》)
short foo(short a, short b) { short result; result = b; while(b > 0) { result *= a; b -= a; } return result; }
不同优化级别的汇编结果
使用gcc14.2分别以-O1和-O3选项编译该代码,得到如下汇编代码:
-O1优化版本
foo: movl %esi, %eax movl %esi, %ecx testw %si, %si jle .L1 .L3: imull %edi, %eax subl %edi, %ecx testw %cx, %cx jg .L3 .L1: ret
-O3优化版本
foo: movl %esi, %eax movl %esi, %edx testw %si, %si jle .L1 .L3: subl %edi, %edx imull %edi, %eax testw %dx, %dx jg .L3 .L1: ret
疑问
在-O3优化下,subl指令位于imul指令之前——这种重排为何有益?
- 我推测这与指令级并行(Instruction-Level Parallelism)有关,但不清楚具体原因。是否先执行减法可让乘法的部分操作利用空闲ALU完成?
- 从流水线(pipelining)角度看,这种重排是否会产生差异?
解答
这种指令重排核心是为了最大化CPU的指令级并行能力,同时规避流水线的延迟问题,具体可以拆解来看:
指令级并行(ILP)的充分利用
整数乘法指令(imul)属于高延迟运算,x86架构中通常需要3-5个时钟周期才能完成结果写入;而减法指令(subl)是单周期指令,几乎能立刻完成。
由于subl操作的是%edx(对应原代码的b),imul操作的是%eax(对应原代码的result),二者没有数据依赖,把subl放在imul前面,CPU可以在等待乘法运算完成的这段时间里,并行执行减法操作,充分利用闲置的算术逻辑单元(ALU)。如果按照-O1的顺序先执行乘法,CPU在乘法运行的几个周期里,减法单元会处于空闲状态,白白浪费计算资源。流水线延迟的隐藏
CPU的流水线将指令拆分成取指、译码、执行、写回等多个阶段。对于乘法这类长延迟指令,一旦进入执行阶段,后续有依赖的指令会被阻塞,但这里subl和imul无依赖,重排后能让它们的流水线阶段重叠。
举个简单的时间线对比:- 原顺序(
-O1):T0周期启动imul,T3-T5周期完成;T3周期才能启动subl,T4周期完成。 - 重排后(
-O3):T0周期可同时发射subl和imul(CPU超标量架构支持并行发射无依赖指令),T1周期完成subl,T3-T5周期完成imul。单轮循环的耗时直接减少1-2个时钟周期,循环次数越多,性能提升越显著。
- 原顺序(
另外,这种重排完全安全,因为两个指令的操作数独立,不会改变程序的语义,最终计算结果和原代码逻辑完全一致。
内容的提问来源于stack exchange,提问作者muser
相关产品推荐
相关产品推荐

