2.3 VLIW 与 EPIC:把调度交给编译器


2.3 VLIW 与 EPIC:把调度交给编译器

本节摘要:本节考察一条激进的路线:与其让硬件在运行时费电地猜哪些指令能并行,不如让编译器在编译期把能并行的指令直接打包。VLIW 与它的改良版 EPIC 在 DSP 领域大获成功,在通用处理器上却折戟沉沙。读完你应当能解释静态调度的收益与致命弱点,并说出判断一种负载是否适合 VLIW 的两条标准。

当编译器接管调度

2.1 节结尾提过,现代处理器靠乱序执行动态地找并行。动态调度很灵活,但代价不菲:记分板、重命名寄存器、保留站这些调度机构要吃掉大量晶体管和功耗,而且经常白忙一场——大多数程序里并行机会本来就不难找。于是自然有一个反问:既然编译器看得到全部代码,为什么不让它提前把"哪些指令可以一起执行"排好班,硬件只管照单执行?

这就是超长指令字(VLIW)的立意。VLIW 机器把若干条能并行的小指令捆成一个固定宽度的超长指令字,比如每字容纳四条运算加两条访存。编译器做全局分析,把互不冲突的指令填进同一字,冲突的指令排进后续字;硬件删去了全部动态调度机构,只留规整的执行单元阵列。指令译码极简、功耗极低、面积极小——在理论世界里,这是一台完美的机器。

; VLIW 风格示意(伪汇编):每行一个超长字,分号分隔同字内的并行槽位 load r4, [r10] ; nop ; nop mul r5, r4, r7 ; add r6, r8,r9 ; nop mul r6, r5, r7 ; add r10,r5,r6 ; store [r11], r6 ; 编译器已保证:同一行内的指令无数据冲突,执行单元各就各位

EPIC:给理想打上补丁

纯 VLIW 太脆——只要指令与槽位一一死绑,代码就与具体硬件形状焊死了。换一代芯片、执行单元数目变了,旧二进制全废。英特尔与惠普在 1990 年代设计 Itanium 时提出的 EPIC(显式并行指令计算)给理想打了三块补丁:打包与语义分离(用模板字段声明"这一组可并行",而非硬绑槽位)、谓词执行(给指令挂条件寄存器,消除大量分支,让编译器敢做大胆优化)、推测加载(提前加载可能用到的数据,出错就丢弃)。128 位的指令束装三条 41 位指令,配合模板位,看起来兼顾了并行表达与一定的向后空间。

2001 年 Itanium 上市,结局众所周知:通用市场惨败。教科书把原因归给生态(x86 软件存量太庞大、微软中途撤出支持),但技术层面的失败同样深刻,而且更值得工程师记住——静态调度赢不了不可预测的访存。编译器排班时以为下一次访存命中缓存,实际却没命中:动态调度的乱序处理器会自动把后面的指令提前填补空窗,VLIW 处理器却只能干等,因为班表是死的。通用程序的访存行为在运行时才揭晓,任何编译器都无法预知。Itanium 的教训因此成为体系结构的公共财富:并行可以静态编排,访存延迟只能动态兜底。

VLIW 在哪里活得很好

把"负载可预测"这个前提补回去,VLIW 立刻满血。数字信号处理(DSP)是最典型的沃土:滤波、编解码这类负载循环紧凑、依赖关系清晰、编译器对执行路径了然于胸。德州仪器的 C6x 系列 DSP 凭八槽 VLIW 在通信设备里服役至今,高通 Hexagon DSP(基带与 AI 推理的心脏)也采用 VLIW 风格的核心。AI 加速器更把这条路线推到极致:张量指令 + 显式软件流水,调度完全交给编译器与算子库,硬件不做任何投机——因为它服务的负载(大矩阵乘)形状稳定、访存模式规整,正是静态排班的理想对象。

由此可以提炼判断负载适不适合 VLIW 的两条标准:其一,执行路径与数据依赖是否编译期可确定(循环边界清晰、分支少);其二,访存延迟是否可控(数据局部性好、或干脆用软件流水把访存预取排进班表)。两条都满足,VLIW 的省电优势巨大;任何一条不满足,动态调度的兜底能力就成了刚需。这也是为什么你的手机里两种流派并存:应用处理器用乱序核心伺候不可预测的通用负载,基带与 NPU 用 VLIW 核心伺候规整的信号与张量负载——不是谁淘汰了谁,而是按负载各就各位。

容易踩的坑

最隐蔽的坑是把 VLIW 的"并行槽位"误当成"必须填满"。编译器排不满班就填 nop(空操作),所以 VLIW 代码里大量空槽是常态,代码密度也远不如表面看起来那么高——Itanium 平均有效位率常被吐槽。第二个坑是以为 EPIC 的谓词执行消灭了分支预测负担:它消的是短分支,长循环与间接跳转照旧存在,预测器照样要有。第三个坑写给 AI 编译器使用者:算子库里手工调优的软件流水正是一门"手工 VLIW 调度"手艺——看懂 2.3 的排班思想,再看算子库的手写汇编就不会一头雾水。

把排班表画出来:一次完整的静态调度

本节的排班思想值得亲手过一遍。假设有这段伪代码要放进两槽的 VLIW 机器(一条乘法槽、一条加法槽,乘法延迟两拍):

; 源语义 a = b * c ; 乘法,结果两拍后可用 d = e + f ; 加法,独立 g = a + d ; 依赖 a 与 d h = i + j ; 独立

编译器的排班:第一行装乘法(算 a)与加法(算 d),乘法的空窗用加法填得很满;第二行装加法 g = a + d(此时 a 已出结果)与 h = i + j——两条独立加法同槽并行。四条运算两行装完,没有空槽。假如代码改成 g = a * d(g 也走乘法槽),第二行的乘法必须等第一行的乘法两拍后出炉,排班变成三行、含空槽——依赖链每深一层,班表就长一截。这个推演把 5.2 节的依赖链概念提前具象了:静态班表的质量完全由依赖图决定,编译器能做的是把独立操作均匀撒进空槽。

Itanium 的指令束还留了一个值得认识的细节:束内的三条指令用模板位声明"槽位类型与束边界",硬件不必逐条猜测哪里断束——这是对纯 VLIW"槽位死绑"的主要松绑。但模板种类有限,编译器凑不齐同类型指令时还是要填 nop,平均有效槽位率因此上不去。设计精细如 EPIC,也逃不开"并行度受限于最宽单元组合"的物理事实。

常见疑问

问:GPU 是 VLIW 吗?不是,但两者常被混为一谈。GPU 靠海量线程并行(线程级并行)掩盖延迟,每条线程内部还是普通标量指令流;VLIW 是指令级并行的静态打包。AMD 早年的 GPU 用过 VLIW 式着色器架构,后来也换成了标量 SIMT 单元——历史包袱给"VLIW 万能论"泼过冷水。真正的当代继承者是 DSP 与 AI 加速器的软件流水:把循环展开成固定节拍,编译器排班,硬件照跑。

问:RISC 指令集加上 VLIW 打包可行吗?可行且真有人做过——早期一些嵌入式 RISC 核就提供 VLIW 模式。难点不在打包而在承诺:一旦把调度冻结进二进制,微架构升级的自由度就没了,这与 RISC 阵营"实现自由发挥"的立意相冲。所以主流 RISC 架构都把静态调度限制在编译器的性能优化层(指令重排),不进编码层。

本节要点回顾

  • VLIW 把调度从硬件搬到编译器:硬件极简省电,代价是班表焊死在二进制里。
  • EPIC 三补丁:模板位解耦打包、谓词消分支、推测加载藏延迟——Itanium 仍因通用负载的不可预测访存而败。
  • 静态调度能编排并行,兜不住访存延迟:这是 Itanium 留下的最贵教训。
  • VLIW 的沃土在负载可预测处:DSP、基带、AI 加速器是今天的活标本。
  • 两条判断标准:依赖编译期可确定、访存延迟可控——满足才上 VLIW。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U