ILP是CPU单核内并行执行不依赖指令的能力,由编译器通过指令重排实现;需-O2/-O3配合目标架构启用,易受真实/输出/反依赖及假依赖破坏,效果须验于汇编。

ILP 不是程序员写的代码,而是编译器调度指令时的优化机会
指令级并行(ILP)指 CPU 在单个核心内,**同时执行多条不相互依赖的指令**的能力。它和多线程、SIMD 都不同:不需要你显式开线程,也不需要手动写 _mm256_add_ps;它是编译器在生成汇编时,通过重排 mov、add、mul 等指令顺序,填满 CPU 流水线空闲槽(如等待内存加载完成的周期),让多个功能单元(ALU、FPU、LSU)并行干活。
编译器靠 -O2/-O3 和目标架构启用 ILP 相关调度
Clang/GCC 默认不激进做指令重排,除非你打开优化等级并指定目标微架构。比如:
-
-O2启用基本的指令调度(如延迟隐藏),但保守; -
-O3加上-march=native或-mcpu=skylake才会启用更激进的跨基本块调度、寄存器 renaming 暗示、软件流水(software pipelining)等; - 若用
-mtune=haswell -march=core2,编译器会按 Core2 的流水线建模,但实际在 Haswell 上跑可能反而更慢——因为模型不准,调度失效。
典型效果:一段含内存加载依赖的循环,编译器可能把下一次迭代的 mov eax, [rbx] 提前到当前迭代的 add ecx, edx 后面,只要地址不冲突,CPU 就能并发发出这两个访存/计算指令。
容易被忽略的 ILP 破坏点:数据依赖和虚假依赖
哪怕编译器想调度,以下情况会让 ILP 失效:
立即学习“C++免费学习笔记(深入)”;
- 真实依赖:
a = b + c;→d = a * 2;,第二条必须等第一条写完a,无法并行; - 输出依赖:
a = b + c;→a = d - e;,编译器不敢乱换序,怕覆盖中间值(虽然后者逻辑上可删前者,但需先做冗余消除); - 反依赖:
a = b + c;→b = d * 2;,第二条改了b,影响第一条读,也不能随意调换; - “假依赖”最隐蔽:
mov eax, 1→mov ax, 2,后者只写低 16 位,但老式 x86 会清高 16 位,导致 CPU 认为eax被全量修改,阻塞后续用eax的指令——现代编译器会插movzx或用and eax, 0xffff拆解来破除。
看懂 ILP 效果得看汇编,不是看 C++ 原文
你写的 for (int i = 0; i ,在 -O3 -march=native 下,GCC 可能生成带 4 路展开 + 向量化 + 指令交错的汇编:连续 4 组 vmovups、vmulpd、vaddpd 交织排列,而不是朴素的“加载→乘→加→下标+1”串行流。
vmovups ymm0, [rax] vmovups ymm1, [rax+32] vmulpd ymm0, ymm0, ymm2 vmulpd ymm1, ymm1, ymm3 vaddpd ymm4, ymm4, ymm0 vaddpd ymm5, ymm5, ymm1 ...
这种交错不是为了“看起来快”,而是让每个 vaddpd 发生在前一个 vmulpd 还在计算时——利用乘法单元延迟约 4–5 周期的窗口,把加法塞进去。没这一步,CPU 大部分时间在等乘法结果,吞吐掉一半。
真正难的是:当你的数据有 cache miss、分支预测失败、或用了 std::vector::at() 带边界检查时,再好的 ILP 也救不了——编译器再聪明,也调度不了停在 L3 缓存外的指令。











