2. 编译基础
各位同学好,我是你们这门课的主讲。今天咱们聊聊编译基础,这部分内容可以说是整个NPU编译优化的地基。你想想看,没有扎实的编译基础,后面那些高级优化技巧根本玩不转。
我个人习惯把编译器想象成一个翻译工厂。前端负责读懂源语言,后端负责输出目标机器码,而中间那层IR,就是工厂内部的半成品。嗯,这个比喻虽然简单,但很实用。
1. 编译器前端:从源码到IR
前端干的事,说白了就是词法分析、语法分析、语义分析这三板斧。词法分析把代码切成token,语法分析构建抽象语法树(AST),语义分析检查类型对不对、作用域合不合理。
我在项目中遇到过一个问题:某个团队自己写了个前端,结果语义分析没做好,导致生成的IR里类型信息丢失。后来调试NPU算子时,死活找不到性能瓶颈在哪。最后发现是前端把float和half类型搞混了,IR里全是隐式转换。
前端输出的AST,一般不会直接用于优化。因为AST太贴近源语言结构,不利于做跨语句的变换。所以编译器会把它降级成更底层的表示——也就是IR。
2. 中间表示(IR):编译器的核心枢纽
IR是编译器的灵魂。没有IR,前端和后端就是两座孤岛。IR的设计好坏,直接决定了你能做哪些优化。
常见的IR有三种形式:
- 三地址码(TAC):每条指令最多三个操作数,比如
a = b + c。简单直观,适合做数据流分析。 - 静态单赋值形式(SSA):每个变量只赋值一次。这玩意儿太重要了,后面讲优化时会反复提到。
- 控制流图(CFG):把基本块用边连起来,表示程序的执行路径。
我个人最喜欢SSA形式。为什么?因为它让数据依赖关系一目了然。你想想看,传统IR里一个变量被赋值多次,你得做def-use链分析才能搞清楚谁用了谁。SSA直接告诉你:这个值只在这里定义,其他地方想用都得通过Phi节点。
这里我画了一张图,展示编译器前端、IR、后端之间的关系:
3. 后端优化:把IR变成机器能懂的话
后端要做的事情,就是把优化后的IR翻译成目标架构的指令。这里涉及三个关键步骤:
- 指令选择:把IR操作映射到目标指令。比如一个加法,在NPU上可能映射成向量加法指令。
- 指令调度:重排指令顺序,避免流水线停顿。NPU的流水线比CPU还深,调度不好性能直接崩。
- 寄存器分配:把无限虚拟寄存器映射到有限物理寄存器。NPU的寄存器文件通常很大,但分配策略不对照样溢出。
我记得有一次做NPU后端优化,指令选择阶段没处理好,把矩阵乘法拆成了一堆标量乘加。结果NPU的矩阵单元完全没利用上,性能比CPU还差。后来改成直接映射到矩阵指令,性能提升了20倍。
4. LLVM与MLIR框架
说到编译器框架,LLVM是绕不开的。它提供了一套完整的工具链,从前端(Clang)到IR(LLVM IR)再到后端,一应俱全。LLVM IR采用SSA形式,支持丰富的优化Pass。
但LLVM有个问题:它的IR是面向CPU设计的。对于NPU这种异构架构,LLVM IR的表达力不够。比如张量操作、数据流模式、内存层次结构,LLVM IR很难直接描述。
这时候MLIR(Multi-Level Intermediate Representation)就派上用场了。MLIR允许你定义自己的IR方言(Dialect),每一层IR可以针对不同的抽象级别。比如:
- 顶层:用Tensor Dialect描述张量计算
- 中间层:用Linalg Dialect描述线性代数操作
- 底层:用LLVM Dialect生成目标代码
我在实际项目中用过MLIR做NPU编译。说实话,刚开始觉得这东西太复杂,方言定义、转换Pass、降级规则,一堆概念。但用顺手之后发现,MLIR的灵活性是LLVM没法比的。你可以针对NPU的特定硬件特性,自定义一个Dialect,然后一步步降级到可执行代码。
这里我整理了一个对比表格,方便你理解LLVM和MLIR的区别:
| 特性 | LLVM | MLIR |
|---|---|---|
| IR层级 | 单层(LLVM IR) | 多层(自定义Dialect) |
| SSA支持 | 原生支持 | 原生支持 |
| 张量操作 | 需通过外部库 | 内置Tensor Dialect |
| 硬件抽象 | 通用CPU/GPU | 可自定义硬件Dialect |
| 优化Pass | 固定Pass管线 | 可组合、可扩展 |
| 适用场景 | 传统编译器 | 异构计算、AI编译器 |
嗯,这里要注意一点:MLIR并不是要取代LLVM。实际上,MLIR的底层通常会降级到LLVM IR,然后利用LLVM的后端生成目标代码。两者是互补关系,不是竞争关系。
最后总结一下今天的核心内容:编译器前端负责把源码变成IR,IR是优化的核心载体,后端负责把IR变成机器码。LLVM提供了成熟的单层IR框架,而MLIR通过多层IR设计,让NPU编译变得更加灵活。我个人建议,如果你刚开始接触NPU编译,先从LLVM入手,理解基本的IR和Pass机制。等遇到LLVM搞不定的场景,再引入MLIR。
好了,今天就到这里。记住,编译基础是后面所有优化技巧的根基,别急着跳过去看高级内容。把IR吃透了,后面事半功倍。