1. 编译器基础

大家好,我是你们这堂课的主讲人。咱们今天聊点基础的,但也是整个课程的地基——编译器。很多做AI加速的同学,一上来就扎进算子融合、内存优化这些细节里,结果发现底层逻辑没搞懂,越往后越吃力。我个人习惯是,先把“编译器到底在干什么”这件事理清楚,后面再讲NPU上的那些花活,你才能理解为什么这么设计。

什么是编译器?说白了就是翻译官

编译器,本质上就是一个翻译程序。它把人类能读懂的代码(比如C++、Python),翻译成机器能执行的指令(比如二进制码)。

你想想看,我们写代码时用的是变量、循环、函数这些高级抽象。但CPU或者NPU只认识0和1,以及一些简单的操作码。这中间的鸿沟,就是编译器来填的。

核心定义:编译器 = 高级语言 → 低级语言的转换器 + 优化器。

我在项目中遇到过不少新人,觉得编译器就是个“翻译工具”,没什么技术含量。其实不然。一个好的编译器,不仅要翻译得对,还要翻译得巧。同样的C代码,GCC和LLVM编译出来的性能可能差30%以上。这就是优化的力量。

编译器三阶段:前端、中端、后端

现代编译器架构,几乎都遵循“三段式”设计。这个划分很巧妙,它让编译器变得模块化、可复用。

我画了一张图,帮你快速建立整体认知:

编译器三段式架构 前端 (Frontend) 词法分析 语法分析 语义分析 输出: AST / IR 中端 (Middle end) IR 优化 常量折叠 死代码消除 输出: 优化后 IR 后端 (Backend) 指令选择 寄存器分配 指令调度 输出: 目标机器码 输入: 源代码 → 前端 → 中端 → 后端 → 输出: 目标代码

前端:读懂你的代码

前端负责把源代码变成一种中间表示(IR,Intermediate Representation)。它主要干三件事:

  • 词法分析:把代码拆成一个个“单词”(token)。比如 int a = 10; 会被拆成 int、a、=、10、;。
  • 语法分析:检查这些单词的组合是否符合语法规则。比如 int a = ; 这种,语法分析阶段就会报错。
  • 语义分析:检查逻辑上的合理性。比如 float a = "hello";,类型不匹配,语义分析会拦住它。

前端输出的IR,是平台无关的。这意味着,你写一份C代码,换一个目标平台(比如从x86换成ARM),前端这部分基本不用动。嗯,这里要注意,不同语言的前端差别很大,但IR的设计目标是一致的——方便后续优化。

中端:核心优化战场

中端是编译器里最“烧脑”的部分。它只操作IR,不关心具体硬件。常见的优化包括:

  • 常量折叠: a = 3 + 5 直接变成 a = 8。
  • 死代码消除:去掉那些永远不会执行的代码。
  • 循环优化:比如循环展开、循环交换,减少循环开销。

我刚开始做编译器时,觉得中端优化就是“锦上添花”。后来在一个图像处理项目里,一个简单的循环展开优化,让处理速度提升了4倍。从那以后,我再也不敢小看中端了。

个人经验:中端优化的好坏,直接决定了编译器的“智商”。LLVM之所以流行,很大程度上是因为它的中端优化框架设计得极其优雅,支持自定义pass。

后端:适配目标硬件

后端把优化后的IR,转换成目标机器的指令。这里要考虑硬件的具体特性:

  • 指令选择:把IR操作映射到具体的机器指令。比如一个加法操作,在x86上可能是 ADD,在ARM上可能是 ADD 或 ADDS。
  • 寄存器分配:决定哪些变量放在寄存器里,哪些放在内存里。寄存器快但数量有限,这是个经典的“取舍”问题。
  • 指令调度:重排指令顺序,让CPU的流水线更顺畅,减少停顿。

我曾经在一个DSP项目上,因为寄存器分配没做好,导致生成的代码性能比手写汇编差了5倍。后来手动调整了分配策略,才追回来。所以说,后端优化是“细节决定成败”。

AI领域的特殊需求:编译器的新挑战

传统编译器是为通用计算设计的。但AI模型,尤其是深度学习模型,给编译器带来了全新的挑战。我总结了几点:

传统编译器 AI编译器
优化标量计算(单个数字) 优化张量计算(多维数组)
目标:CPU/GPU通用指令 目标:NPU/TPU专用指令
控制流复杂(if-else、循环) 数据流为主(矩阵乘、卷积)
优化目标:延迟 优化目标:吞吐量 + 内存带宽
手动编写优化规则 自动搜索 + 机器学习辅助

说白了,AI编译器要处理的是“大块头”数据。一个卷积层,可能涉及几百兆字节的数据搬运。传统编译器那套“寄存器分配”策略,在NPU面前基本不够看。

为什么会这样?因为NPU的架构和CPU完全不同。NPU通常有大量的计算单元(比如MAC阵列),但控制逻辑简单。编译器需要把模型的计算图,切分成适合NPU硬件执行的“子图”,还要考虑数据在片上和片外的搬运策略。

避坑指南:我曾经在一个项目里,直接把PyTorch导出的ONNX模型扔给NPU编译器,结果性能惨不忍睹。后来才发现,模型里有一些很小的算子(比如逐元素加),在NPU上反而成了瓶颈。正确的做法是,先做算子融合,把多个小算子合并成一个大的计算核。

另外,AI编译器还有一个特殊需求——自动调优。传统编译器里,优化策略是工程师写死的。但在AI领域,同样的卷积操作,在不同尺寸、不同硬件上,最优的实现方式可能完全不同。所以现代AI编译器(比如TVM、XLA)都引入了自动搜索机制,让编译器自己去试各种优化组合,找到最优解。

嗯,这里要强调一下,AI编译器不是要取代传统编译器,而是在传统编译器的基础上,增加了针对张量计算和专用硬件的优化层。很多AI编译器,底层还是依赖LLVM或者GCC来做代码生成。

好了,这一章的内容就到这里。编译器的基础概念,是后面所有优化技术的地基。你理解了前端、中端、后端的分工,就能明白为什么算子融合放在中端做,而内存分配放在后端做。下一章,我们会深入AI编译器的核心——计算图优化,到时候你会看到这些基础概念是如何落地的。