来源于小伙伴提问。
代码A:经过完美优化,没有指令间依赖导致的停顿。在这种情况下,即使乱序执行引擎重排指令,最终的指令顺序和原本顺序会大体相同,因为代码已经被优化到最小依赖性。对于代码A,乱序执行能进一步提升的空间较小,因为没有额外的指令重排能够提高并行性。 代码B:存在依赖,如果按顺序执行会有停顿。乱序执行引擎在处理代码B时,可以重新安排指令的执行顺序,来隐藏这些依赖关系引起的停顿。虽然代码B原本的顺序较差,但是乱序执行可以通过重排指令使得性能接近代码A的水平。
乱序执行的额外开销:虽然乱序执行可以提升性能,但重排指令、跟踪依赖关系、硬件重命名寄存器等操作本身是有代价的。如果代码A已经完美优化,在乱序执行时需要的重排和依赖处理会更少,相对来说能更好地利用CPU资源。 乱序窗口的限制:乱序执行有一个窗口(out-of-order window),只能在窗口范围内的指令中进行重排。如果代码B的依赖关系较为密集,乱序窗口可能不足以完全消除停顿。
减少乱序执行引擎的负担:手动优化代码可以减少乱序执行过程中对指令重排和依赖分析的需求,使得CPU执行更为高效。例如,如果能够手动消除依赖关系或者调整指令顺序,就能减少乱序执行的重排开销。 提升并行性:乱序执行的硬件能力是有限的,手动优化代码可以更好地利用多执行单元的并行能力。例如,交错使用整数运算和浮点运算指令,或者同时执行内存访问和计算操作。 硬件特性:不同的CPU对乱序执行的支持程度不同,优化代码可以更好地针对特定硬件特性。例如,有些老旧或低功耗CPU的乱序执行能力较弱,这种情况下代码的手动优化显得尤为重要。
减少数据依赖性:尽量减少指令之间的数据依赖,例如通过增加指令间的运算、缓存临时结果到寄存器来减少对之前指令结果的依赖。 减少内存访问延迟:内存访问是指令停顿的主要来源之一,可以通过软件预取(prefetching)、增加缓存命中率(合理使用数据结构)等手段,降低访问延迟。 避免寄存器重命名冲突:乱序执行依赖寄存器重命名技术来消除伪依赖(false dependency)。可以通过合理安排寄存器使用,减少重命名冲突。 利用指令并行性:在指令间隙中插入无关操作,使得更多的指令可以并行执行。例如,将计算指令与加载指令交错安排,减少流水线的停顿。 合理使用分支预测:尽量减少分支错误预测带来的流水线清空,重排代码或者避免难以预测的分支。