
编译原理 — 第6.2节 三地址代码
在编译器的中间代码生成阶段,三地址代码(Three-Address Code, TAC)是一种重要的中间表示形式。它的主要特点是:每条指令的右侧至多包含两个操作数,因此无法表示诸如 x + y * z 这样的复杂表达式,必须拆解为一系列简单指令,如:
t1 = y * z
t2 = x + t1
这里的 t1 和 t2 称为临时变量(temporary names),由编译器生成,用于表示中间结果。三地址代码在形式上可以是语法树、DAG,或是线性指令序列。
图 6-8 展示了一个表达式的 DAG 表示以及对应的三地址代码指令序列。
6.2.1 地址和指令
三地址代码的基本组成单位是地址(address)和指令(instruction):
-
地址包括变量名、临时变量、常量或数组元素;
-
指令描述一种基本操作,例如赋值、算术、跳转、调用等。
常见的三地址指令形式如下:
-
赋值:
x = y op z,表示执行二元运算后赋值; -
一元运算:
x = op y; -
直接赋值:
x = y; -
无条件跳转:
goto L; -
条件跳转:
if x relop y goto L; -
函数调用:
-
param x:传递参数; -
call p, n:调用过程; -
return y:返回结果;
-
-
数组操作:
-
x = y[i]:读取数组; -
x[i] = y:写入数组;
-
-
指针操作:
-
x = &y、x = *y、*x = y等。
-
图 6-9 给出了三地址指令的两种编码方式:
-
符号标号编码(如:
if t3 < v goto L); -
位置标号编码(如:
if t3 < v goto 100)。
6.2.2 四元式表示
**四元式(quadruple)**是记录三地址代码的结构化表示方式,每个指令由四个字段组成:
-
op:操作符; -
arg1、arg2:操作数; -
result:结果变量名。
例如表达式 a = b * -c + b * -c 对应如下四元式:
(1) minus, c, -, t1
(2) *, b, t1, t2
(3) minus, c, -, t3
(4) *, b, t3, t4
(5) +, t2, t4, t5
(6) =, t5, -, a
图 6-10 展示了该表达式的三地址代码与四元式表示形式。
6.2.3 三元式表示
**三元式(triple)**类似于四元式,但省略了 result 字段,其结果通过三元式编号来间接引用。每条指令以三元式编号标识,其字段如下:
-
op:操作; -
arg1、arg2:位置编号或常量。
三元式的优点在于不依赖变量名,引用统一用编号表示。例如:
(0) minus, c, -
(1) *, b, (0)
(2) minus, c, -
(3) *, b, (2)
(4) +, (1), (3)
(5) =, (4), -
三元式的结构简洁、实现高效,但缺点是如果中间指令顺序发生改变,其引用位置也需相应修改。
6.2.4 静态单赋值形式(SSA)
SSA(Static Single Assignment)形式是一种更进一步的中间表示要求:程序中每个变量只能被赋值一次,每个赋值产生一个新的变量名。为实现 SSA,编译器引入 φ 函数,在控制流汇合点选择不同分支下的赋值来源。
例如:
if (flag) x = 1;
else x = 2;
y = x * a;
转为 SSA 后为:
x1 = 1
x2 = 2
x3 = φ(x1, x2)
y = x3 * a
SSA 形式简化了数据流分析,是现代优化编译器的标准中间表示。
6.2.5 6.2节的练习
练习 6.2.1:将表达式 a - (b + c) 分别翻译为:
-
抽象语法树
-
四元式序列
-
三元式序列
-
间接三元式序列
练习 6.2.2:对下列赋值语句重复练习 6.2.1:
a = b[i] + c[j];
a[i] = b * c + b * d;
x = f(y + 1) + 2;
x = *p + &y;
练习 6.2.3:说明如何对一个三地址代码序列进行转换,使每个被赋值的变量都有唯一的变量名。
下面给出参考答案。
按照教材要求,每条题目依次给出
-
抽象语法树(用 Mermaid 渲染,若博客平台支持 Markdown-Mermaid,直接复制即可);
-
四元式序列;
-
三元式序列;
-
间接三元式序列(indirect triple)。
说明
四元式字段顺序固定为
(序号) op, arg1, arg2, result。三元式省略
result字段,引用上一行的结果直接用行号(0-based)。间接三元式在左栏列出行号,再在右侧列出相同顺序的三元式行指针。
练习 6.2.1
将表达式
a - (b + c)翻译为四种表示
① 抽象语法树
graph TD
SUB[-] --> A[a]
SUB --> ADD[+]
ADD --> B[b]
ADD --> C[c]
② 四元式
| # | op | arg1 | arg2 | result |
|---|---|---|---|---|
| 1 | + | b | c | t1 |
| 2 | - | a | t1 | t2 |
③ 三元式
| # | op | arg1 | arg2 |
|---|---|---|---|
| 0 | + | b | c |
| 1 | - | a | (0) |
④ 间接三元式
| 行号 | instruction |
|---|---|
| 35 | (0) |
| 36 | (1) |
练习 6.2.2
语句 1 a = b[i] + c[j];
抽象语法树
graph TD
ASS[=] --> A1[a]
ASS --> ADD1[+]
ADD1 --> Bidx[b[i]]
ADD1 --> Cidx[c[j]]
四元式
| # | op | arg1 | arg2 | result |
|---|---|---|---|---|
| 1 | [] | b | i | t1 |
| 2 | [] | c | j | t2 |
| 3 | + | t1 | t2 | t3 |
| 4 | = | t3 | - | a |
三元式
| # | op | arg1 | arg2 |
|---|---|---|---|
| 0 | [] | b | i |
| 1 | [] | c | j |
| 2 | + | (0) | (1) |
| 3 | = | (2) | a |
间接三元式
| 行号 | instruction |
|---|---|
| 50 | (0) |
| 51 | (1) |
| 52 | (2) |
| 53 | (3) |
语句 2 a[i] = b * c + b * d;
抽象语法树
graph TD
ASS2[=] --> IDX[a[i]]
ASS2 --> ADD2[+]
ADD2 --> MUL1[*]
ADD2 --> MUL2[*]
MUL1 --> B1[b]
MUL1 --> C1[c]
MUL2 --> B2[b]
MUL2 --> D1[d]
四元式
| # | op | arg1 | arg2 | result |
|---|---|---|---|---|
| 1 | * | b | c | t1 |
| 2 | * | b | d | t2 |
| 3 | + | t1 | t2 | t3 |
| 4 | []= | a | i | t3 |
三元式
| # | op | arg1 | arg2 |
|---|---|---|---|
| 0 | * | b | c |
| 1 | * | b | d |
| 2 | + | (0) | (1) |
| 3 | []= | a | i,(2) |
间接三元式(行号 60 起)
| 行号 | instruction |
|---|---|
| 60 | (0) |
| 61 | (1) |
| 62 | (2) |
| 63 | (3) |
语句 3 x = f(y + 1) + 2;
抽象语法树
graph TD
ASS3[=] --> X[x]
ASS3 --> ADD3[+]
ADD3 --> CALL[f(...)]
ADD3 --> CONST2[2]
CALL --> PLUS1
PLUS1 --> Y[y]
PLUS1 --> ONE[1]
四元式
| # | op | arg1 | arg2 | result |
|---|---|---|---|---|
| 1 | + | y | 1 | t1 |
| 2 | param | t1 | - | - |
| 3 | call | f | 1 | t2 |
| 4 | + | t2 | 2 | t3 |
| 5 | = | t3 | - | x |
三元式
| # | op | arg1 | arg2 |
|---|---|---|---|
| 0 | + | y | 1 |
| 1 | param | (0) | - |
| 2 | call | f | 1 |
| 3 | + | (2) | 2 |
| 4 | = | (3) | x |
语句 4 x = *p + &y;
四元式
| # | op | arg1 | arg2 | result |
|---|---|---|---|---|
| 1 | *deref | p | - | t1 |
| 2 | & | y | - | t2 |
| 3 | + | t1 | t2 | t3 |
| 4 | = | t3 | - | x |
三元式、间接三元式同理,可按行号 (0)…(3) 建立。
练习 6.2.3
如何把一段三地址代码序列转换,使每个被赋值的变量都有唯一的名字
方法(即 Static Single Assignment / 变量重命名流程):
-
扫描原始三地址序列一次,维护一个映射
name → version。 -
遇到赋值指令
name = ...时:-
取
version++,把左值改写为name_version; -
将映射更新到新版本。
-
-
遇到读取
name时,查当前映射并用最新版本号替换为name_version。 -
控制流分叉:在每个基本块入口插入
φ函数,将来自不同前驱块的同名不同版本值合并(如教材 6.2.4 SSA 规则)。 -
反复应用直至整个控制流图所有赋值都拥有唯一变量名,得到单赋值形式的三地址代码。
例:
原序列x = a + b; x = x * c;
重命名后:x₁ = a + b x₂ = x₁ * c若控制流中存在
if (...) x = 1 else x = 2 y = x + 3则需引入
φ:x₁ = 1 x₂ = 2 x₃ = φ(x₁, x₂) y = x₃ + 3
完成后,每个变量只写一次,便于后续分析与优化。
理解
一、理论理解(结合教材结构 + 表达变换本质)
在编译器中,三地址代码(TAC)作为中间表示(IR)的一种形式,其本质是把高层语言中的复杂语法结构展开为一个低阶、等价且易优化的线性中间代码形式,这种线性形式同时保留了操作数依赖、执行顺序和基本控制流语义,方便后续进行数据流分析、优化和目标代码生成。
教材中将 TAC 拆解为四种关键变体:
-
三地址线性代码(最基本):将复杂表达式转为多条形式固定的
x = y op z。 -
四元式:适合数据结构表示,每一条指令用四个字段统一封装,可建表。
-
三元式:压缩结构更紧凑,用编号引用中间结果,避免重复变量命名。
-
SSA:静态单赋值形式,是现代编译器如 LLVM 的核心表示方式,有利于优化。
这些变体的存在反映出一个底层规律:中间表示的演进=表达能力 × 分析难度 × 优化便利性之间的权衡。
在练习题 6.2.1~6.2.3 中可以看到:
-
表达式
a - (b + c)看似简单,转换过程中仍需显式拆解出t1 = b + c才能保证结构的线性化。 -
表达式
a = b[i] + c[j]体现出数组元素的间接寻址,必须借助[]运算指令; -
在 SSA 中对变量重命名的要求与 φ 函数的引入,是理解控制流与数据依赖的关键一步。
因此,三地址代码的理解核心在于:将程序结构“线性化 + 显式化”,以服务于自动化优化和目标代码生成。
二、大厂实战理解(结合 LLVM、昇腾 MindIR、Google Triton 编译器)
在工业编译器系统中,TAC 或其衍生形式广泛应用于实际系统的中间表示(IR),不同大厂在此层的技术细节各有特长:
✅ LLVM 系统(Apple、Google、NVIDIA、AMD)
-
使用 SSA-based IR,每个变量只赋值一次,配合 LLVM Pass 执行各种高级优化(如常量传播、公共子表达式消除、死代码删除等)。
-
与四元式不同的是,LLVM IR 是强类型的,形如:
%1 = add i32 %a, %b。 -
练习 6.2.3 的变量重命名就是 SSA 的核心机制。
✅ 华为昇腾 MindSpore & MindIR
-
使用图式中间表示(静态图为主),节点语义类似于三地址指令,但表达的是图节点间的数据依赖。
-
图优化 Pass 中经常通过**公共子表达式消除(CSE)**将
b * c + b * d优化成共用b的结构,借助 DAG 与三元式思想。 -
MindIR 构图过程中也有 SSA 的影子,例如每一个 Tensor 节点都是一次写入、多次读取。
✅ Google Triton 编译器
-
针对 AI 模型中的张量表达式进行“内核级表达展开”,使用结构上接近四元式的
ir::instruction体系。 -
实现中构造了一种“高维线性中间表示”,其中表达式
f(y+1)+2必须转化为 SSA 形式,便于统一 loop fusion。
✅ ByteDance Volcano Engine + Babel Compiler
-
使用 SSA DAG + 节点指纹缓存构建表达 DAG,有效实现跨 batch 表达式重用,这正是练习题中表达式
b * c + b * d中公共子表达式问题的工程级实现版本。
理解
一、理论理解:三地址代码的本质是“可控的中间语言”
在源程序编译为目标代码的过程中,三地址代码(TAC)提供了一种可等价、可优化、可映射的中间表示语言。它的设计思路可以总结为两句话:
-
一是将复杂表达式转化为线性、单步、显式的指令序列;
-
二是为优化与目标代码生成留出足够的中间空间和结构冗余。
教材中通过以下四种表达形式讲清了三地址代码的设计本质:
-
线性三地址码:适合从语法树或 DAG 直接翻译,每条指令最多涉及两个操作数,如
t1 = a + b。 -
四元式表示:添加结构化字段(op, arg1, arg2, result),方便统一管理与优化索引。
-
三元式表示:取消结果变量名,改用位置编号,压缩表示空间,但带来顺序耦合。
-
SSA(Static Single Assignment):每个变量只赋值一次,适合数据流分析与寄存器分配。
而练习题 6.2.1~6.2.3 的设置正是围绕这四者展开:
-
表达式
a - (b + c)展示了基本表达式的“结构 → 中间码”映射过程; -
诸如
x = *p + &y涉及数组、指针、函数等复合语言结构的编码; -
6.2.3练习则直接引出 SSA 的命名规约原理 —— 强制唯一命名是“优化之源”。
在逻辑上,这四种形式的抽象能力是从“线性 → 表结构 → 紧凑 → 高级抽象”的逐层提升,体现了三地址码设计的系统性演化。
二、大厂实战理解:TAC 是现代编译器优化的中枢表示层
在工业界主流编译系统中,三地址代码或其变体(尤其是 SSA)已成为不可或缺的核心组件。
✅ LLVM:统一基于 SSA 的中间表示
LLVM 的每一条中间指令本质上都是一个三地址操作(甚至更规整),如:
%1 = add i32 %a, %b
%2 = mul i32 %1, %c
-
所有
%name变量在 LLVM 中仅赋值一次,形成 SSA; -
基于 SSA 可以轻松实现诸如 GVN(全局值编号)、Loop-Invariant Code Motion(LICM)等优化;
-
在练习 6.2.3 中强制变量名唯一性,正是工业 SSA 的重命名规则基础。
✅ Google V8 TurboFan:指令图优化
在 V8 JavaScript 引擎中,TurboFan 将 JavaScript 编译为一种称为 Sea-of-Nodes 的 DAG 图结构,该图的每一个节点就是一个 TAC 操作,支持:
-
懒执行;
-
类型收窄(type-narrowing);
-
JIT 回退点等高级策略。
✅ 华为 MindSpore/MindIR:图式三地址结构
在 MindIR 的构图过程中,一个神经网络前向表达 x = W*x + b 会被拆分为:
t1 = matmul(W, x)
t2 = add(t1, b)
-
每一个
t是三地址临时变量; -
生成 DAG 后,再做优化(如消除冗余计算、重排张量布局);
-
表达式
a = b[i] + c[j]对应的数组索引变换在 MindIR 中通过“广播节点 + Index 算子”封装。
这些系统共同的底层逻辑是:将程序语义压缩为稳定、便于分析的数据流表示 —— TAC 是其中最关键的通用形式。
三、自研编译器视角:三地址码是你能“控制优化”的第一个支点
当你试图从零开发一个自己的解释器或编译器,你一定会经历这样一个转折点:
你发现用语法树走到了尽头 —— 它无法承载更多优化、也无法映射到底层指令。
这时你意识到,需要构造一个 “结构可遍历 + 步进可控 + 可记录依赖” 的中间语言,这就是 TAC 的作用。它让你可以:
-
显式管理每一步运算(如:先计算乘法、再加法);
-
控制变量生命周期(临时变量
t1什么时候创建、什么时候销毁); -
添加跳转、判断、函数调用等语义,而不依赖源语言结构。
而当你进一步开发一个 AI 编译器,如将 ONNX 图编译为张量指令时:
-
你需要把
x = f(y + 1) + 2拆为:t1 = y + 1 t2 = call f, t1 x = t2 + 2 -
同时你还需要把参数类型、调用协议(参数寄存器/栈)、返回值行为建模进去;
-
如果你采用 SSA,你还会加上版本控制:
y1 = y + 1 f1 = call f, y1 x1 = f1 + 2
在这整个过程中,三地址码就是你构造 IR、调试语义、实现优化、生成汇编的第一跳。你甚至可以为每个三地址码分配唯一 ID,做 Debug Symbol 映射 —— 这也正是 GCC 与 Clang 内部的调试辅助机制。
四、总结一句话理解
三地址代码的学习本质上是训练“如何把高级语言语法转换为线性依赖表达”的思维能力,只有构造出细粒度的线性代码,编译器的自动优化才有可能被触发;无论是做 JIT、静态优化还是 AI 模型编译,三地址代码思想都是 IR 设计的底座。
大厂面试题:三地址代码篇(6.2)
🧠 面试题 1:请你解释一下三地址代码的设计初衷是什么?为什么编译器不直接基于语法树生成目标代码?
参考答案:
三地址代码的设计初衷,是为了在保留原始程序语义的基础上,将复杂的语法结构线性展开为等价的中间形式,从而为后续的优化与目标代码生成提供可控且结构清晰的基础表达;如果编译器直接从语法树生成汇编或机器码,虽然理论上可行,但由于语法树无法显式表达中间值、控制流或数据依赖等信息,极大地限制了分析和优化的能力,尤其是在面对现代编译器要求的大量 IR 优化(如公共子表达式消除、循环展开、寄存器分配等)时,三地址码作为“更接近底层、更适合转换”的中间层,成为必须引入的桥梁。
🧠 面试题 2:请对比四元式、三元式和静态单赋值形式(SSA),它们分别适用于哪些场景?有何优缺点?
参考答案:
四元式、三元式和 SSA 都是三地址代码的不同变体,核心目标一致——结构清晰地表达中间操作,但在应用场景与优化便利性方面各有取舍:四元式以四字段结构明确表达操作符、两个操作数及结果,适用于中间代码生成阶段,且支持独立结果名管理;三元式通过位置引用方式消除显式变量名,适用于更紧凑的表示结构,但不利于调试或跨指令依赖分析;而 SSA 是一种更高级的表示形式,核心在于每个变量仅赋值一次,天然支持数据流分析、值追踪与寄存器分配,是 LLVM、Graal 等现代编译器的基础 IR 表示形式。总的来说,SSA 表达能力最强,四元式工程实践最广,三元式最紧凑但实用性有限。
🧠 面试题 3:假如你需要实现一个支持三地址码的优化器,请问你会如何识别公共子表达式并进行优化?
参考答案:
识别公共子表达式的关键在于对已有三地址指令序列构造值编号表或 DAG 图结构,并通过等价匹配判断当前表达式是否已存在副本;具体而言,可以为每条三地址指令构建 (op, arg1, arg2) 的哈希键,并记录其结果变量,若后续出现同样结构的表达式,则可以复用之前的结果变量,避免重复计算,这一过程对应于编译器中的公共子表达式消除(CSE)优化策略;在工程实践中,LLVM 通过构建 SSA DAG 自动实现此类优化,而如 GCC 则通过 gimple 层四元式分析实现相似的子表达式检测。
🧠 面试题 4:如何从三地址代码过渡到机器指令?中间是否需要额外的表示层?
参考答案:
从三地址代码过渡到机器指令通常需要一个目标代码生成器,将三地址操作映射为具体架构指令;不过在许多现代编译器中,为了实现更灵活的调度与寄存器分配,通常会引入一种“目标无关的底层中间表示(Lower IR)”,如 LLVM 的 SelectionDAG 或 MachineIR,它进一步接近机器语言的形式,支持控制流图、物理寄存器与调度单元的建模;因此三地址码更像是“前端 IR”,适用于语言语义处理与优化阶段,而最终的目标代码还需额外构建调度与寄存器分配层以贴合底层硬件。
🧠 面试题 5(开放类):如果让你用三地址码来表示深度学习模型中的前向传播,请你简单说明整体流程。
参考答案:
若用三地址码表示深度学习模型的前向传播过程,可将每一个算子(如 MatMul、ReLU、Add 等)视为一条三地址操作指令,其输入为上游计算图节点,输出为中间张量变量;例如对于 Y = ReLU(W * X + b),可以转换为:
t1 = MatMul(W, X)
t2 = Add(t1, b)
Y = ReLU(t2)
这种方式可进一步拓展为 SSA 形式便于依赖追踪与内存重用,实际上很多 AI 编译器(如 XLA、ONNX Runtime、MindIR)内部正是借助三地址结构或其图式变体来构建整个模型推理图、并据此做调度与张量重用优化,因此 TAC 是 AI 编译器的中间结构核心之一。
场景题 1:你在字节跳动的服务端编译组中负责一个 Lua 到字节码的中间表示优化模块,发现多个函数的三地址码序列中存在大量重复结构如 t1 = b + c; t2 = a - t1; t3 = b + c; t4 = d - t3,导致优化器识别不到公共子表达式,请你分析这个问题并提出解决方案。
参考回答:
面对三地址代码中多个语义等价却冗余存在的指令序列问题,我首先会判断当前优化流程中是否缺失了公共子表达式消除(CSE)或值编号(Value Numbering)模块;从给出的样例可以看出,表达式 b + c 被重复生成为 t1 和 t3,但由于变量名不同,优化器未能识别其等价性,因此我会设计一个值编号系统:为每条三地址指令的 (op, arg1, arg2) 构建哈希键,并使用全局哈希表记录其结果变量名;当后续指令出现相同操作时,即可直接复用之前变量,替代冗余指令生成,这不仅能减少代码体积,还能显著提升寄存器分配的效率;同时我也会检查当前 SSA 构建过程是否允许临时变量重复命名,若存在版本混淆问题,则必须引入统一的静态单赋值规则以保证优化通用性和正确性。
场景题 2:你在阿里云 PAI 平台中开发自研 DSL 的编译器后端,发现在将 DSL 编译为中间三地址代码的过程中,由于缺乏 SSA 支持,后续数据流分析和死代码删除失效,导致执行图中存在大量不可达变量,请你说明如何构造 SSA 并修复这一问题。
参考回答:
为了解决因缺少 SSA(静态单赋值)而导致数据流分析失效的问题,我会从 IR 层构建流程着手进行系统性调整;首先我会为三地址指令序列增加唯一变量命名的规范,在每次赋值语句中通过版本编号方式对变量重命名,例如将多次对 x 的赋值依次标记为 x1, x2, x3 等;其次,在控制流图中识别所有基本块的汇合点,并在这些位置引入 φ 函数用于合并多条分支路径中的同名变量,从而保证每个变量在语义上只有唯一来源;这一机制使得每个值的定义点可以明确追踪,其使用范围清晰界定,从而支撑常见优化如常量传播、死代码删除和冗余消除等;最终构造出的 SSA 形式不仅提升了分析精度,还能为寄存器分配和调度打下基础,解决原有中间代码不可达变量积压的问题。
场景题 3:你在昇腾 AI 编译器团队中负责 MindIR 图优化模块,发现某个训练模型经过三地址码生成后,每个表达式都分配了独立变量,导致张量复制和内存写操作频繁,推理性能下降,请问你如何优化三地址代码的变量生成与生命周期?
参考回答:
针对三地址代码中临时变量生成过多导致张量复制与内存压力上升的问题,我会从两个层面展开优化:第一,在变量命名策略上引入 SSA-based 变量合并机制,通过值追踪判断哪些变量在逻辑上可以复用同一个内存空间,从而在 SSA 基础上生成等价的生命周期压缩图(Live-Range Graph),供后续内存调度器复用内存块;第二,在三地址码生成阶段引入 reuse-aware 策略,当某个变量在其生命周期终止后,如果下一个操作拥有相同 shape 和类型,我会直接复用原变量名或其分配空间,从而避免不必要的张量复制与内存分配;同时,我也会结合控制流图分析,在基本块边界插入 SSA φ 节点,进一步收敛变量定义数量,最终让整个 MindIR 的表达结构不仅保持逻辑正确性,也具备高效的运行时内存行为。
场景题 4:你在 OpenAI Triton 编译器项目中参与高性能矩阵计算表达式的优化,现有三地址码中存在大量函数调用形如 t1 = call f, x; t2 = t1 + 2;,但在实际代码中出现多个不同版本的 call f,编译器重复计算影响吞吐,请问你如何提升三地址函数调用的结构复用性?
参考回答:
为优化三地址码中函数调用的结构复用性,特别是在高性能场景下避免相同调用结果的重复计算,我会引入“调用表达式签名缓存机制”,即对所有 call f, args... 类型指令构造基于函数名与实参哈希的唯一签名,在第一次调用时执行并缓存结果变量,在后续相同签名出现时直接复用此前结果变量或其 SSA 编号;此外,在 SSA 图构建过程中,我也会标记函数调用节点为 pure 或 impure,其中 pure 节点允许安全复用(如无副作用函数),而 impure 则强制保留独立路径,防止语义错误;这种策略既保证了计算图的正确性,又极大提升了代码复用度,降低冗余指令数和缓存访问成本,尤其适用于像 Transformer block 中大量重复结构的深度学习模型代码生成过程。
场景题 5(AI 编译器研发视角):
你正在从零研发一套用于深度学习模型加速的 AI 编译器系统,完成了前端 DSL 的词法与语法分析模块后,现在进入中间代码生成阶段,目标是将模型表达式如 x = W * (input - bias) + b 转换为结构合理、可调度、可优化的中间表示;但你发现三地址码在表达张量操作时指令过长且中间变量冲突频繁,严重影响后续优化和调试效率。请你描述你是如何设计三地址码结构、管理临时变量,并为下游调度与代码生成做好准备的。
参考回答:
在从零设计 AI 编译器中间代码生成系统时,我首先明确三地址码的作用不仅是翻译表达式,更是整个优化与调度阶段的结构基础,因此面对深度模型表达如 x = W * (input - bias) + b,我会优先将其解析为运算树或语法 DAG,再通过自顶向下的递归翻译策略生成三地址序列,例如:t1 = input - bias; t2 = W * t1; t3 = t2 + b; x = t3;,这一序列化过程确保指令线性、可控,便于后续流图构建。
为了管理中间变量,我设计了一个基于作用域上下文的临时变量分配系统,其中每个临时变量如 t1, t2 带有生命周期标注,配合计数器与 SSA 编号控制其唯一性,避免变量名冲突,确保在复杂图优化(如张量融合或 operator inlining)中仍能准确追踪数据依赖;同时,我构建了完整的 def-use chain 与控制流图(CFG),记录每条指令的定义点与使用点,为 Dead Code Elimination、Common Subexpression Elimination 等优化模块提供精确依赖信息;在进入调度阶段前,我还为每个三地址码节点生成静态成本估计(如张量大小、内存读写、算子延迟),并将 IR 序列进一步转换为 SSA 格式,提供给后端目标映射器做设备适配与调度排序。
通过这种方式,我不仅构建了从高层表达式到三地址代码的稳定中间桥梁,还确保 IR 拥有良好的结构局部性、可分析性与调度友好性,为后续整个 AI 编译器在多设备部署、多图优化与性能调度方面奠定了结构基础。

1941

被折叠的 条评论
为什么被折叠?



