传统编译器系列 - 第1节 编译器的基础概念

32 阅读13分钟

ChatGPT Image 2025年6月26日 21_48_31.png


传统编译器系列 - 第1节 编译器的基础概念

本节内容基于华为 Ascend & MindSpore 平台的编译器课程内容,结合传统编译器理论与现代实践,系统讲解编译器与解释器的区别、JIT/AOT 编译方式、Pass 和 IR 中间表示等核心概念,是学习 GCC、LLVM、AI 编译器的入门前置章节。


一、编译器与解释器的区别

编译器(Compiler)与解释器(Interpreter)是将高级语言转化为可执行代码的两种主要形式。两者的本质差异如下:

对比维度编译器(Compiler)解释器(Interpreter)
执行方式编译后整体执行边解释边执行
输出生成目标代码不生成中间目标代码
存储保留中间码(可执行文件)不保存中间码
错误处理一次性显示全部错误遇到错误才停止并提示
执行效率高(提前编译)低(解释执行)
示例语言C/C++、Rust、Go、Java(AOT)Python、Ruby、PHP、JavaScript

简言之,编译器负责一次性翻译并生成机器码,而解释器负责运行时逐条解析与执行代码。


二、JIT 与 AOT:两种编译策略

AOT(Ahead of Time)提前编译

程序在运行前一次性全部编译为机器码,例如:

  • C 语言通过 GCC 编译生成 ELF 文件
  • Go 程序通过 go build 编译生成二进制

优点:

  • 启动速度快,运行效率高
  • 没有运行时编译开销

缺点:

  • 无法动态优化执行路径

JIT(Just in Time)即时编译

程序在运行时动态地将热点代码编译为机器码,例如:

  • Java 使用 HotSpot 虚拟机动态 JIT 优化热点函数
  • PyTorch 在训练后使用 TorchScript 将模型 JIT 编译为优化图

优点:

  • 可根据运行时信息进行优化(例如 CPU 缓存、指令重排)
  • 支持动态类型和运行时生成代码

缺点:

  • 启动慢,首次运行需编译
  • 更高的内存与 CPU 消耗

对比图示:

编译模式启动速度最终性能应用场景
AOT稳定工业部署、嵌入式
JIT灵活AI 推理、浏览器、虚拟机

三、Pass 与 IR:编译器的中间表示结构

什么是 Pass?

Pass 是指编译器在处理源码时的一个阶段性转换逻辑。每一个 Pass 负责某种特定功能,如:

  • 词法分析(Lexical Analysis)
  • 语法分析(Syntax Analysis)
  • 语义检查(Semantic Analysis)
  • 中间代码生成(IR Generation)
  • 优化处理(如常量折叠、死代码消除)
  • 目标代码生成(Code Generation)

一个现代编译器通常包含数十甚至上百个 Pass。

什么是 IR(Intermediate Representation)?

IR 是介于源代码与机器码之间的一种中间层代码形式。它抽象出平台无关性,同时也适合进行各种优化。常见 IR 类型:

  • LLVM IR:文本或 SSA(Static Single Assignment)形式
  • GCC 的 GIMPLE / RTL
  • TensorFlow 的 MLIR(用于 AI 编译器)

结构示意图:

           High-Level Source Code
                     ↓
         ┌────────────────────────────┐
         │ Lexical → Syntax → Semantic│
         │       → IR Generator        │
         └────────────────────────────┘
                     ↓
         Intermediate Representation
                     ↓
         Target Code Generator (e.g. x86, ARM)
                     ↓
               Machine Code

IR 的价值在于,它解耦了“源语言”与“目标架构”的强绑定,使得:

  • 一个编译器可支持多语言(如 Clang 支持 C/C++/ObjC)
  • 一个语言可支持多平台(如 LLVM 可后端编译为 x86/ARM/NVPTX)

四、传统编译器的工作模型

以下是传统编译器的工作示意:

A. 全流程编译模式

Source Code → Compiler → Machine Code → Output
                        ↓
                  Error Messages(静态检测)

B. 解释器执行模型

Source Code → Interpreter → Output(每一行逐行执行)
                     ↺
             Get Next Instruction

这体现出传统编译器更关注性能与优化,而解释器关注交互与灵活性。


五、IR 的生成与用途(图示补充)

如图所示:

  • 词法分析器、语法分析器与语义分析器负责将源代码分阶段转化为结构化中间代码
  • IR 可被优化器读取并进行变换,再交由代码生成器输出目标平台代码

图示引用:

           High-Level Language
                    ↓
    ┌────────────────────────────┐
    │ Lex → Syntax → Semantic → │
    │     Intermediate Generator │
    └────────────────────────────┘
                    ↓
             Intermediate Code
                    ↓
              Target Code(可执行)

六、小结

概念含义
编译器将源代码转换为机器码的程序
解释器实时读取并执行源代码的程序
JIT即时编译:运行时优化执行路径
AOT提前编译:部署稳定性强,启动快
Pass编译流程中的阶段性处理器
IR编译器内部通用中间表示语言,便于优化与平台适配

💡理解 | 编译器基础概念

🎓 理论理解

在计算机语言的发展历史中,编译器与解释器的划分构成了程序运行机制的两个基本分支:编译器强调静态转换后执行,解释器强调动态逐句执行。这种执行模型的差异决定了它们在不同场景下的适配性。

  • 编译器(Compiler) :更适合追求性能、稳定性、类型安全的语言,例如 C/C++、Go,它们通过 AOT(Ahead-of-Time)编译方式,将源码在运行前一次性转换为平台相关的机器码,执行速度快但缺乏灵活性;
  • 解释器(Interpreter) :则适用于动态语言(如 Python、Ruby),强调灵活、交互、快速原型迭代,但运行时性能相对较低。

同时,**JIT 编译(Just-in-Time)**的引入在很大程度上弥合了两者的差距,它保留了解释器灵活性并在运行时动态生成最优机器码,是现代虚拟机(如 JVM、V8)的核心机制之一。

进一步地,IR(Intermediate Representation) 的提出极大推动了编译器模块化和平台无关性的设计理念:通过统一的中间代码层,源语言与目标平台得以解耦,从而实现“一次前端、多后端”策略。LLVM 的成功即建立在强大的 SSA 结构 IR 基础上。

编译器中的 Pass 架构也体现了软件工程模块化设计的思想,每一个 Pass 只承担一个明确职责(词法分析、语义分析、优化等),利于维护、调试与扩展。

🏢 大厂实战理解(华为昇腾 / Google / OpenAI / NVIDIA)

✅ 华为昇腾(Ascend)

本课程正是基于华为昇腾 AI 芯片生态所打造,昇腾平台强调模型部署的性能与可控性,因此:

  • 在编译策略上以 AOT 为主,结合离线优化编译与多级 IR 表达(如 MindIR → KernelIR);
  • 引入了 Pass 驱动优化、图算融合与静态调度,确保部署前模型最优化;
  • 其 MindSpore 框架通过解释型动态图 + 编译型静态图结合设计,体现了解释器与编译器混合驱动架构。

华为编译器强调部署时的确定性与安全性,因此更依赖 AOT 和静态分析,同时也发展 IR 驱动的图级优化逻辑(如 AutoTune、内存复用)。

✅ Google(TensorFlow / XLA)

Google 的 AI 编译体系构建于 XLA(Accelerated Linear Algebra)编译器之上,强调多阶段 IR 优化:

  • 源代码 → TensorFlow Graph IR → HLO IR(High Level Optimizer) → LLVM IR;
  • 动态语言层(如 Python API)在前端运行,但最终将编译成高效的、设备相关的低层指令;
  • 部分模型训练路径采用 JIT 编译方式(如 TF XLA JIT),以提升硬件适配与性能。

Google 强调编译器的深度优化能力,通过高层 IR 提供分析信息、图优化,再配合 LLVM 与硬件代码生成,达到高效运行。

✅ OpenAI(训练平台)

OpenAI 内部模型训练平台使用大量容器沙箱化机制,为了确保资源隔离和安全,通常将用户模型定义解释执行,而核心调度、模型下发等操作则通过 AOT 编译代码完成。

  • 前端以 Python + 动态图解释为主;
  • 中间调度层通过编译器(如 Triton 编译器)生成 GPU 核函数(IR → PTX);
  • 支持向量化、多卡并行、自动数据对齐等特定 Pass 插入。
✅ NVIDIA(CUDA / TensorRT)

NVIDIA 的编译器体系则完全基于深度优化的 IR 系统构建:

  • 使用 CUDA 编译器(nvcc)进行前端处理;
  • TensorRT 将深度学习模型转化为高度优化的 GPU 执行计划,依赖多阶段 IR 与融合 Pass(如 layer fusion、kernel tuning);
  • 同时支持 JIT 插件机制,将用户自定义操作在 runtime 编译后注入推理图中。

📌 总结一句话

现代编译器不仅是“源代码翻译器”,更是软硬件协同调优的智能调度核心,其底层架构正在从传统静态编译走向“多级中间表示 + 动态执行路径 + 深度优化 Pass”的全栈式智能编译系统。


七、课程思维导图(建议配图)

你可以自行画出如下思维结构:

编译器基础
├─ 编译 vs 解释
├─ JIT vs AOT
├─ Pass / 阶段转换
└─ IR:中间表示
    ├─ 表达优化
    ├─ 平台无关
    └─ 后端生成目标代码

🧠 大厂面试题 | 编译器的基础概念


面试题 1:请简要说明编译器与解释器的主要区别,它们适合于哪些类型的语言或场景?

参考回答:
编译器与解释器的核心区别在于代码执行的时机与方式不同:编译器将整个程序在运行前一次性翻译为目标机器代码(AOT),适用于 C、C++、Go 等对性能和类型安全要求高的语言;解释器则在运行过程中逐行翻译和执行源代码,适合脚本语言如 Python、Ruby、JS 等,便于快速开发与交互调试。

在工程实践中,底层驱动程序、嵌入式系统、操作系统内核通常采用 AOT 编译器部署,而数据分析、AI 实验平台和 Web 环境则更偏好解释型语言结合 JIT 优化。


面试题 2:什么是 IR?为什么现代编译器一定需要引入 IR 这一中间表示层?

参考回答:
IR(Intermediate Representation)是介于高级源语言与底层机器代码之间的中间代码形式,它具备平台无关性结构表达性强的优势,使得编译器得以分离前端语言解析与后端代码生成,支持“一次前端、多平台后端”的目标构建模式。

在现代编译体系中(如 LLVM、XLA、TVM),IR 作为优化与调度的中枢,可以以 SSA 结构表达数据依赖关系,便于实现诸如常量折叠、死代码消除、算子融合、调度重排等优化 Pass,是支撑静态编译、JIT 加速与跨平台部署的关键组件。


面试题 3:请对比 AOT 和 JIT 编译的优势与劣势,并说明各自适用于哪些系统场景?

参考回答:
AOT(Ahead of Time)在编译期完成所有代码转换,生成稳定的目标文件,具有启动快、执行快、易于部署的优势,适用于嵌入式系统、系统软件与深度学习推理部署(如 Ascend 离线模型编译)。
JIT(Just in Time)则在运行时根据输入数据与硬件状况动态编译热点路径,具有适应性强、可运行时优化等特点,适合 AI 框架、浏览器引擎、图形渲染等需要动态优化的场景(如 PyTorch JIT、TensorFlow XLA)。

JIT 的劣势是启动慢、内存消耗大;AOT 的劣势则是缺乏运行时上下文信息,优化空间有限。


面试题 4:现代编译器为什么采用多 Pass 架构设计?有哪些工程上的优势?

参考回答:
采用多 Pass 架构是为了将编译器的不同阶段(如词法分析、语法解析、语义检查、优化、代码生成)功能模块化,从而带来以下工程优势:

  1. 职责单一:每个 Pass 只处理一类问题,便于调试与测试;
  2. 高度复用:通用 Pass 可被多个前端语言或后端平台复用;
  3. 插拔式扩展:可以动态启用/禁用特定优化逻辑,适应不同部署需求;
  4. 调度灵活:Pass 顺序可根据目标平台进行调整,实现渐进式优化(如 TensorRT 的 kernel fusion Pass)。

例如 LLVM PassManager 支持动态注册 Pass 队列,MindSpore GraphEngine 中的优化器也以 Pass 链方式构建模型图优化管线。


面试题 5:假设你现在要设计一个编译器框架来支持 C/C++ 和 Python 两种语言,你会如何组织前端、中间表示与后端架构?

参考回答:
我会采用多前端、统一 IR、多后端的三层编译结构:

  1. 前端:使用 Clang 处理 C/C++ 源码,使用 Python AST 分析器处理 Python 代码,统一输出中间表示;
  2. IR 层:设计一个基于 SSA 的中间表示(如 LLVM IR),用于表达控制流、数据流与操作符;
  3. 后端:根据目标平台分别生成 x86、ARM、CUDA、Ascend Kernel 等目标指令码;
  4. 调度器:插入 Pass 管理器,支持静态优化、算子融合、图调度等通用处理。

这种架构可参考 LLVM、TVM 和 MindSpore 的设计思路,实现高扩展性与跨语言支持。

场景题 1:

你在昇腾团队负责图算融合优化的编译子系统,现在遇到一个模型在进行 IR Pass 时生成的中间表示图结构冗余,导致 Kernel 拆分严重,性能下降明显。你如何定位并优化这个问题?


参考答案:

在面对图结构冗余导致算子拆分与性能下降这一问题时,我首先不会直接去调试后端代码生成逻辑,而是选择从中间表示构图的前链入手,具体会在 IR Dump 阶段开启详细的结构日志,结合 Graph Visualizer 或 IR Viewer 工具,从图的构建初期开始逐层追踪节点生成与流转路径,以定位哪些节点是冗余添加、哪些是重复计算,尤其是关注常见的未融合算子模式如 Broadcast→Cast→ReshapeTranspose→MatMul 等链式结构是否被错误拆解。接着,我会结合当前 FusionPass 执行的调度顺序审视是否存在优化 Pass 执行次序不当的问题,例如是否先执行了形状推理 Pass 导致图结构提前展开,从而影响后续的算子融合逻辑失效。针对这一点,我会尝试在 Pass Manager 中重排图优化顺序,把与算子融合相关的 FusionPass 提前到图拓扑稳定后再执行,从而提升融合率。

与此同时,我还会重新检查当前 Fusion 模板的匹配逻辑是否过于保守,比如当前模板是否只允许 Conv→BN→ReLU 严格连续而未考虑 BN 被转化为 Scale+Shift 形式后导致融合失败,此时我会扩展 Pattern Engine 的匹配规则,允许更灵活的图模式识别。同时,在中间表示被构建之前,如果模型导出源是 Python 脚本或 ONNX、MindIR 等图描述语言,我也会从源端对原始图进行清洗,移除显式保留但实际无效的 IdentityNoOp 操作,减少对后续 IR 图的污染。

最终,在完成融合规则调整、Pass 调度重排以及 IR 构图源头清洗后,我会通过重新编译和运行模型,观察 kernel 拆分数量是否减少、Launch 次数是否降低,并结合 Profiling 工具验证推理延迟是否下降。在一次真实场景中,我通过上述优化使得某视频超分模型的 kernel 数从原来的 280 降至 170 左右,整体推理延迟下降了超过 15%,显著提升了昇腾端到端部署的效率与能耗表现。


image.png