Day 2:前端技术 + 数据流与支配分析

目标:理解词法/语法分析原理,掌握数据流分析和支配分析的框架——这是优化和后端的数学基础。

今天的内容分两大块:上午用龙书建立前端理论(源码→AST→IR),下午用 LLVM 书建立分析框架(数据流+支配+循环)。后者是 Day3 写优化 Pass、Day4 做寄存器分配的前置知识。


上午:前端技术(龙书第 3-5 章)

1. 词法分析(第 3 章)—— 把字符流切成 Token

1.1 词法分析器长什么样

编译器看到的是原始字符流,词法分析器(Lexer/Scanner)把它切成有意义的 Token:

输入字符流:  "x = 42 + y\n"
输出Token流: IDENT(x)  ASSIGN  NUMBER(42)  PLUS  IDENT(y)

每个 Token 是一个二元组 (类型, 值)

// Token 的数据结构(C 语言表示)
typedef struct {
    int   type;     // IDENT, NUMBER, PLUS, IF, WHILE, ...
    char *lexeme;   // 原始字符串,比如 "42", "foo"
    int   line;     // 行号(用于报错)
} Token;
1.2 核心管线:正则 → NFA → DFA → 最小化 DFA

为什么不能直接在源码上搜字符串?因为 Token 是正则语言——恰好能用有限自动机精确识别:

正则表达式 ──Thompson构造──→ NFA(非确定有限自动机)
                              │
                              │ 子集构造法
                              ↓
                            DFA(确定有限自动机)
                              │
                              │ Hopcroft算法
                              ↓
                         最小化 DFA

第一步:从正则表达式构造 NFA(Thompson 构造法)

规则只有 5 条,每条按图拼接:

基本规则:
  ε(空串):   (s0)──ε──→((s1))
  字符 a:      (s0)──a──→((s1))

复合规则:
  串联 st:      连接 NFA(s) 的终态到 NFA(t) 的初态(用 ε 边)
  选择 s|t:     新增初态,用 ε 边分叉到 NFA(s) 和 NFA(t) 的初态
  星号 s*:      新增初态,用 ε 边到达 NFA(s) 初态;NFA(s) 终态再 ε 回初态

以正则 a(b|c)* 为例,Thompson 构造过程:

Step 1: 'a'          →  (0)──a──→(1)
Step 2: 'b'          →  (2)──b──→(3)
Step 3: 'c'          →  (4)──c──→(5)
Step 4: 'b|c'        →  (6)──ε──→(2)──b──→(3)──ε──→(7)
                          └──ε──→(4)──c──→(5)──ε──→┘
Step 5: '(b|c)*'     →  用两个新状态 + 4条ε边做 Kleene 星
Step 6: 'a(b|c)*'    →  串联 Step1 结果 和 Step5 结果

第二步:NFA → DFA(子集构造法)

NFA 的问题是:同一个状态下读同一个字符,可能有多条出路——程序无法直接执行"猜测"。

子集构造法的思想:把 NFA 状态集合作为一个 DFA 状态

def subset_construction(nfa, alphabet):
    """用 Python 实现子集构造法"""
    start_set = epsilon_closure(nfa, {nfa.start})
    dfa_states = {frozenset(start_set): 0}  # 状态集 -> DFA状态编号
    dfa_trans = {}   # (dfa_state, char) -> dfa_state
    worklist = [start_set]

    while worklist:
        S = worklist.pop()
        s_id = dfa_states[frozenset(S)]
        for c in alphabet:
            # move(S, c): 从 S 中任一状态经字符 c 能到达的状态集合
            T = set()
            for state in S:
                if (state, c) in nfa.transitions:
                    T.add(nfa.transitions[(state, c)])
            # 对 T 中每个状态做 ε-闭包
            T = epsilon_closure(nfa, T)
            if not T:
                continue
            t_fs = frozenset(T)
            if t_fs not in dfa_states:
                dfa_states[t_fs] = len(dfa_states)
                worklist.append(T)
            dfa_trans[(s_id, c)] = dfa_states[t_fs]

    return dfa_states, dfa_trans

def epsilon_closure(nfa, states):
    """从 states 出发,只走 ε 边能到达的所有状态"""
    stack = list(states)
    closure = set(states)
    while stack:
        s = stack.pop()
        for t in nfa.epsilon_transitions.get(s, []):
            if t not in closure:
                closure.add(t)
                stack.append(t)
    return closure

子集构造出来的 DFA 状态数最多是 2^N(N = NFA 状态数),实际通常远小于上限。

第三步:DFA 最小化(Hopcroft 算法)

核心思想:不断把"行为不同"的状态分开(等价类划分)。

def hopcroft_minimize(dfa):
    """Hopcroft 算法:把等价状态合并"""
    # 初始划分:终态一组,非终态一组
    P = [dfa.accepting, dfa.states - dfa.accepting]
    W = [dfa.accepting]  # 工作队列

    while W:
        A = W.pop()  # 分裂子
        for c in dfa.alphabet:
            # X = {s | δ(s,c) ∈ A},即读字符 c 后进入 A 的状态
            X = {s for s in dfa.states if dfa.trans(s, c) in A}
            for Y in P[:]:
                inter = X & Y
                diff  = Y - X
                if inter and diff:
                    # Y 被 X 切分
                    P.remove(Y)
                    P.append(inter)
                    P.append(diff)
                    if Y in W:
                        W.remove(Y)
                        W.append(inter)
                        W.append(diff)
                    else:
                        W.append(inter if len(inter) <= len(diff) else diff)
    return P
1.3 用 flex 写一个词法分析器

不需要手写 DFA,用 flex 声明式描述即可:

/* scanner.l —— 一个迷你 C 语言词法分析器 */
%{
#include <stdio.h>
int line = 1;                  /* 当前行号 */
%}

/* 正则定义(类似宏) */
DIGIT   [0-9]
ID      [a-zA-Z_][a-zA-Z0-9_]*

%%
 /* 规则:正则模式   动作(C代码) */

"int"           { printf("KEYWORD  int\n"); }
"if"            { printf("KEYWORD  if\n"); }
"else"          { printf("KEYWORD  else\n"); }
"while"         { printf("KEYWORD  while\n"); }
"return"        { printf("KEYWORD  return\n"); }

{ID}            { printf("IDENT    %s\n", yytext); }

{DIGIT}+        { printf("NUMBER   %s\n", yytext); }
{DIGIT}+"."{DIGIT}+ { printf("FLOAT    %s\n", yytext); }

"+"             { printf("OP       +\n"); }
"-"             { printf("OP       -\n"); }
"*"             { printf("OP       *\n"); }
"/"             { printf("OP       /\n"); }
"="             { printf("OP       =\n"); }
"=="            { printf("OP       ==\n"); }
"<"             { printf("OP       <\n"); }
">"             { printf("OP       >\n"); }

";"             { printf("SEMI\n"); }
"("             { printf("LPAREN\n"); }
")"             { printf("RPAREN\n"); }
"{"             { printf("LBRACE\n"); }
"}"             { printf("RBRACE\n"); }

[ \t]+          { /* 跳过空白 */ }
\n              { line++; }

"//".*          { /* 跳过单行注释 */ }
"/*"([^*]|\*+[^/*])*"*"+"/" { /* 跳过多行注释 */ }

.               { printf("ERROR line %d: unexpected '%s'\n", line, yytext); }
%%

int main(int argc, char **argv) {
    if (argc > 1) {
        yyin = fopen(argv[1], "r");
    }
    yylex();
    return 0;
}

int yywrap() { return 1; } /* 到文件尾,结束 */

编译运行

flex scanner.l          # 生成 lex.yy.c(C语言词法分析器源码)
gcc lex.yy.c -o scanner # 编译
./scanner test.c        # 对 test.c 做词法分析

flex 生成的 DFA 背后用的就是子集构造法——你写正则,它替你生成高效的状态机。


2. 语法分析(第 4 章)—— 把 Token 流建成语法树

2.1 上下文无关文法(CFG)

词法分析器识别"单词",语法分析器识别"句子结构"。CFG 用四条东西描述一门语言:

CFG: G = (N, T, P, S)
  N: 非终结符集合(语法变量,比如 expr, stmt)
  T: 终结符集合(就是 Token 类型,比如 NUMBER, PLUS, IF)
  P: 产生式集合(重写规则)
  S: 开始符号

例如,一个简单表达式文法:

E → E + T | E - T | T
T → T * F | T / F | F
F → ( E ) | id | num

这个文法有左递归E → E + T),LL 分析器无法处理,必须先消除。

消除左递归的标准手法

原始:  E → E + T | T
       T → T * F | F
       F → ( E ) | id

改写:  E  → T E'
       E' → + T E' | ε
       T  → F T'
       T' → * F T' | ε
       F  → ( E ) | id

现在 E'T' 都用右手边第一个符号做决策——LL 分析器可以工作了。

2.2 手写递归下降 Parser(最常用)

递归下降 = 每个非终结符写一个函数,一眼看懂:

# parser.py —— 递归下降表达式解析器
# 文法(已消除左递归):
#   expr    → term expr_tail
#   exprtail → + term expr_tail | - term expr_tail | ε
#   term    → factor term_tail
#   termtail → * factor term_tail | / factor termtail | ε
#   factor  → ( expr ) | NUMBER | IDENT

class Parser:
    def __init__(self, tokens):
        self.tokens = tokens   # [(type, value), ...]
        self.pos = 0

    def peek(self):
        """看当前 token,不消费"""
        if self.pos < len(self.tokens):
            return self.tokens[self.pos]
        return ('EOF', '')

    def consume(self):
        """吃掉当前 token 并返回"""
        tok = self.peek()
        self.pos += 1
        return tok

    def expect(self, expected_type):
        """吃掉一个 token,类型必须是 expected_type"""
        tok = self.consume()
        if tok[0] != expected_type:
            raise SyntaxError(f"行 {self.pos}: 期望 {expected_type},遇到 {tok}")
        return tok

    # ---- 真正的解析函数 ----

    def parse_expr(self):
        """expr → term expr_tail"""
        left = self.parse_term()
        return self.parse_expr_tail(left)

    def parse_expr_tail(self, left):
        """expr_tail → + term expr_tail | - term expr_tail | ε"""
        if self.peek()[0] == 'PLUS':
            self.consume()
            right = self.parse_term()
            node = ('+', left, right)
            return self.parse_expr_tail(node)  # 左结合:继续递归
        elif self.peek()[0] == 'MINUS':
            self.consume()
            right = self.parse_term()
            node = ('-', left, right)
            return self.parse_expr_tail(node)
        else:
            return left  # ε:没有更多运算符了

    def parse_term(self):
        """term → factor term_tail"""
        left = self.parse_factor()
        return self.parse_term_tail(left)

    def parse_term_tail(self, left):
        """term_tail → * factor term_tail | / factor term_tail | ε"""
        if self.peek()[0] == 'STAR':
            self.consume()
            right = self.parse_factor()
            node = ('*', left, right)
            return self.parse_term_tail(node)
        elif self.peek()[0] == 'SLASH':
            self.consume()
            right = self.parse_factor()
            node = ('/', left, right)
            return self.parse_term_tail(node)
        else:
            return left

    def parse_factor(self):
        """factor → ( expr ) | NUMBER | IDENT"""
        tok = self.peek()
        if tok[0] == 'NUMBER':
            self.consume()
            return ('num', int(tok[1]))
        elif tok[0] == 'IDENT':
            self.consume()
            return ('id', tok[1])
        elif tok[0] == 'LPAREN':
            self.consume()
            node = self.parse_expr()
            self.expect('RPAREN')
            return node
        else:
            raise SyntaxError(f"意外的 Token: {tok}")

# ---- 测试 ----
# 输入: "3 + 4 * 5"
# Token 流: [('NUMBER','3'), ('PLUS','+'), ('NUMBER','4'), ('STAR','*'), ('NUMBER','5')]
tokens = [
    ('NUMBER', '3'), ('PLUS', '+'), ('NUMBER', '4'),
    ('STAR', '*'), ('NUMBER', '5')
]
parser = Parser(tokens)
ast = parser.parse_expr()
# AST 结果: ('+', ('num', 3), ('*', ('num', 4), ('num', 5)))
# 这棵 AST 正确表达了 "乘法优先于加法" 的语义

这段代码可以直接跑,修改 tokens 就能解析不同表达式。递归下降是工业界最常用的 Parser 写法——Clang 的 C 语言 Parser 就是递归下降的。

2.3 LL(1) 分析:FIRST 和 FOLLOW

递归下降的"选择哪个产生式"这件事,可以用FIRST/FOLLOW精确计算:

FIRST(α): 从 α 能推导出的所有串的**第一个终结符**的集合
         (包含 ε,如果 α 能推导出空串)

FOLLOW(A): 在某个句型中,紧跟在 A 后面的**终结符**的集合
          (从来不会包含 ε)

计算方法(不动点迭代)

def compute_first(grammar):
    """迭代法求所有符号的 FIRST 集"""
    first = {sym: set() for sym in grammar.nonterminals + grammar.terminals}
    for t in grammar.terminals:
        first[t].add(t)  # 终结符的 FIRST 就是它自己

    changed = True
    while changed:
        changed = False
        for lhs, rhs_list in grammar.productions:
            for rhs in rhs_list:  # 对产生式 A → X1 X2 ... Xn
                all_nullable = True
                for X in rhs:
                    # FIRST(A) ∪= FIRST(X) \ {ε}
                    old = len(first[lhs])
                    first[lhs] |= first[X] - {'ε'}
                    if len(first[lhs]) > old:
                        changed = True
                    if 'ε' not in first[X]:
                        all_nullable = False
                        break
                if all_nullable:
                    # 所有符号都能推导出 ε,那么 ε ∈ FIRST(A)
                    if 'ε' not in first[lhs]:
                        first[lhs].add('ε')
                        changed = True
    return first

LL(1) 分析表构造

对每条产生式 A → α:
  对 FIRST(α) 中的每个终结符 a(a ≠ ε):
    把 A → α 填入 M[A, a]
  如果 ε ∈ FIRST(α):
    对 FOLLOW(A) 中的每个终结符 b:
      把 A → α 填入 M[A, b]

如果某个格子被填了两次(冲突),说明不是 LL(1) 文法——要么改写文法消除左递归/提公因子,要么换更强的分析技术。

2.4 LR 分析:更强大的自底向上方法

递归下降(LL)是"预测式"的——根据已看到的来猜接下来是什么。LR 则是**“看到右手边足够多了才归约”**——能处理的文法范围更广。

LR 分析器的四个动作:
  移入(shift): 把下一个 token 压入栈
  归约(reduce): 栈顶的一串符号匹配某条产生式右手边 → 替换为左手边
  接受(accept): 归约到开始符号
  报错(error): 语法错误

LR 的状态机是一个 DFA,每个状态维护一个项目集。一个项目 A → α·β 表示"正在识别 A,已经看到了 α,期望看到 β"。

四种 LR 的递进关系

LR(0):完全不看前瞻,只看项目集本身 → 冲突多,实用性有限
  ↓ 加入 FOLLOW 来解决归约/移入冲突
SLR(1):用 FOLLOW(A) 决定是否归约 → 简单,但有时太保守
  ↓ 在前瞻符号上做精确跟踪
LR(1):项目带前瞻符 A → α·β, a → 状态数爆炸(CS 理论最通用)
  ↓ 合并"核心相同"的 LR(1) 状态
LALR(1):LR(1) 的合并版本 → yacc/bison 实际使用的,状态数≈SLR,能力≈LR(1)

一个 LR(1) 项目示例

项目: [A → α·β, a]
意思: 正在识别 A,已看到 α,期望看到 β
      a 是前瞻符——只有下一个 token 是 a 时才能按这个项目归约

用 yacc/bison 写语法分析器

/* calc.y —— 简单计算器(yacc 语法文件) */
%{
#include <stdio.h>
%}

%token NUMBER
%left '+' '-'    /* 左结合,优先级低 */
%left '*' '/'    /* 左结合,优先级高 */

%%
input:
    /* 空 */
  | input line
  ;

line:
    '\n'
  | expr '\n'   { printf("= %d\n", $1); }
  ;

expr:
    NUMBER            { $$ = $1; }
  | expr '+' expr     { $$ = $1 + $3; }
  | expr '-' expr     { $$ = $1 - $3; }
  | expr '*' expr     { $$ = $1 * $3; }
  | expr '/' expr     { $$ = $1 / $3; }
  | '(' expr ')'      { $$ = $2; }
  ;
%%

int main() { return yyparse(); }
int yyerror(char *s) { fprintf(stderr, "错误: %s\n", s); return 0; }

编译运行:

yacc -d calc.y     # 生成 y.tab.c 和 y.tab.h(LALR(1) 分析表)
gcc y.tab.c -o calc
echo "3+4*5" | ./calc   # 输出: = 23

关键理解:yacc 的 %left 不仅指定了结合性,还隐式解决了移入-归约冲突——“看到 + 后栈顶有 3+4,是归约还是移入?→ 左结合 = 归约”。


3. 语法制导翻译(第 5 章)—— 边解析边干活

Parser 建好 AST / 生成 IR 的过程就是语法制导翻译。

3.1 综合属性 vs 继承属性
# 例子:计算表达式的值 + 生成三地址码
class SDT_Expr:
    """
    expr → term expr_tail
      综合属性: expr.val    = term.val + expr_tail.val(从子传父)
      综合属性: expr.code   = term.code + expr_tail.code(连接的 IR)
      继承属性: expr_tail.inherited = term.val(从父/兄弟传子)
    """
    def __init__(self):
        self.code = []    # 三地址码列表

    def new_temp(self):
        """分配临时变量名"""
        self.temp_counter = getattr(self, 'temp_counter', 0) + 1
        return f't{self.temp_counter}'

    def translate_expr(self, node):
        """
        返回 (val_place, code)
          val_place: 存放结果的"位置"(变量名或常量值)
          code: 生成的三地址码列表
        """
        if node[0] == 'num':
            return node[1], []                 # 常量,无需代码

        elif node[0] == 'id':
            return node[1], []                 # 变量名

        elif node[0] in ('+', '-', '*', '/'):
            # 综合属性: left.val 和 right.val 来自子节点
            left_val, left_code  = self.translate_expr(node[1])
            right_val, right_code = self.translate_expr(node[2])

            # 分配临时变量存放结果
            temp = self.new_temp()

            # 生成三地址码
            code = left_code + right_code
            code.append(f'{temp} = {left_val} {node[0]} {right_val}')

            return temp, code   # 返回结果位置 + 代码

# ---- 翻译 ----
# AST: ('+', ('num', 3), ('*', ('num', 4), ('num', 5)))
sdt = SDT_Expr()
result, code = sdt.translate_expr(ast)
# result = 't1'
# code   = ['t1 = 4 * 5', 't2 = 3 + t1']
print('\n'.join(code))

从这个例子理解 Clang 的工作方式:Clang 的 AST 节点(ExprStmtDecl)每个都有语法制导定义的属性。CodeGen 模块(clang/lib/CodeGen/)就是一个巨大的 SDT 翻译器——遍历 C++ AST,生成 LLVM IR。

3.2 S-属性 vs L-属性
S-属性定义L-属性定义
属性流向只从子 → 父从左兄弟或父 → 子
适合什么 ParserLR(自底向上)LL(自顶向下)
遍历方式后序遍历即可求值需要按从左到右的顺序
LLVM 中的例子常量折叠作用域解析(外层符号表传入内层)

实际场景int a = b + 3; 中,a 的类型 int继承属性(从声明往下传),b + 3 的值是综合属性(从子节点往上算)。


下午:数据流分析与支配分析(LLVM 书第 3-5 章)

从这里开始,后面三天的内容(Day2 下午 → Day3 优化 → Day4 后端)全部依赖数据流分析。数据流分析是编译优化的统一数学框架

4. 数据流分析(第 3 章)—— 全称是"基于格的数据流分析框架"

4.1 什么是数据流分析

编译器需要在不实际运行程序的前提下,回答关于程序运行时行为的问题:

我想知道…对应数据流问题
变量 x 在 p 点取什么值常量传播
变量 x 在 p 点之后还用吗活跃变量分析
表达式 a+b 之前算过了吗可用表达式分析
x 的定义在 p 点还能到达吗到达定值分析
4.2 数据流分析的统一框架

所有数据流问题都可以填进同一个模板:

对每个基本块 B:
  in[B]  = ∧ { out[P] : P 是 B 的前驱 }
  out[B] = gen[B] ∪ (in[B] - kill[B])

其中:
  gen[B]  = 块 B 内部"新产生"的事实
  kill[B] = 块 B 内部"杀死"的事实
  ∧       = 交汇运算(∪ 或 ∩,取决于问题类型)

为什么是"格"? 因为数据流值构成一个半格(Semi-lattice)

  • 值域 V:所有可能的数据流值
  • 交汇运算 ∧:合并多条路径的信息,必须满足交换律、结合律、幂等律
  • 偏序 ≤:信息量排序,a ≤ b 表示 a 的信息不比 b 多
  • 顶元素 ⊤:最"保守"的值(什么都不知道)
  • 底元素 ⊥:最"精确"的值(一切都知道)

关键性质:迭代应用 out[B] = gen[B] ∪ (in[B] - kill[B]) 单调递减(或递增),所以一定收敛到最大不动点(MFP)

4.3 实例一:到达定值分析(Reaching Definitions)

问题:在 p 点,变量 x 的值可能是由哪些定义语句赋值的?

方向:  前向(顺着控制流)
交汇:  ∪(汇集所有可能)
gen:   { d | d 是块内定义的语句 }
kill:  { 其他语句对同一个变量的定义 }
初始化: out[ENTRY] = ∅

实例——给定以下 CFG,手算到达定值:

             [B1]
         d1: x = 1
         d2: y = 2
              │
         ┌────┴────┐
        ▼           ▼
      [B2]         [B3]
  d3: x = 3    d4: y = 4
  d4: z = 5    d5: x = 6
        │           │
        └────┬────┘
             ▼
           [B4]
       d6: w = x + y

迭代求解过程

初始化:
  in[B1] = ∅
  out[B1] = gen[B1] = {d1, d2}

第一遍迭代:
  in[B2] = out[B1] = {d1, d2}
  out[B2] = gen[B2] ∪ (in[B2] - kill[B2])
          = {d3, d4} ∪ ({d1, d2} - {d1})    // d3 杀了 d1(都定义 x)
          = {d3, d4, d2}

  in[B3] = out[B1] = {d1, d2}
  out[B3] = {d4, d5} ∪ ({d1, d2} - {d2, d1})  // d4 杀 d2(都定义 y),d5 杀 d1,d3
          = {d4, d5}

  in[B4] = out[B2] ∪ out[B3] = {d3, d4, d2} ∪ {d4, d5} = {d2, d3, d4, d5}
  out[B4] = gen[B4] ∪ (in[B4] - kill[B4])
          = {d6} ∪ ({d2, d3, d4, d5} - ∅)
          = {d2, d3, d4, d5, d6}

第二遍迭代: 所有 in/out 不再变化 → 算法终止

结论:在 B4 的入口,x 可能来自 d3(B2 路径)或 d5(B3 路径),y 可能来自 d2 或 d4。这就是为什么编译器在 B4 不能把 x+y 常量折叠——x 的值不确定。

4.4 实例二:活跃变量分析(Live Variables)

问题:变量 x 在 p 点的值将来还会被用到吗?

方向:  后向(逆着控制流)
交汇:  ∪(汇集所有可能)
def[B]: 块 B 内被**定值**且在定值前未在 B 内被引用的变量
use[B]: 块 B 内被**引用**且在引用前未在 B 内被定值的变量
公式:  in[B] = use[B] ∪ (out[B] - def[B])
初始化: in[EXIT] = ∅

用途:死代码消除(DCE)—— 如果变量定值后不再活跃,这条赋值可以删除。寄存器分配也严重依赖活跃分析——两个变量同时活跃就不能用同一个寄存器。

同个 CFG,手算活跃变量分析(设最终 w 在 EXIT 被引用):

[B4] w = x + y     def={w}, use={x,y}
[B2] x = 3; z = 5  def={x,z}, use=∅
[B3] y = 4; x = 6  def={y,x}, use=∅
[B1] x = 1; y = 2  def={x,y}, use=∅

逆序迭代(从 B4 往 B1):

Pass 1:
  out[B4] = ∅
  in[B4] = {x,y} ∪ (∅ - {w}) = {x,y}     ← x,y 在 B4 用到

  out[B2] = in[B4] = {x,y}
  in[B2] = ∅ ∪ ({x,y} - {x,z}) = {y}     ← 只有 y 从入口活跃到 B2

  out[B3] = in[B4] = {x,y}
  in[B3] = ∅ ∪ ({x,y} - {y,x}) = ∅       ← x,y 都在 B3 内重新定义了

  out[B1] = in[B2] ∪ in[B3] = {y} ∪ ∅ = {y}
  in[B1] = ∅ ∪ ({y} - {x,y}) = ∅         ← x,y 都在 B1 定义

Pass 2: 无变化 → 终止

结论:x 和 y 在 B1 的 out 处只有 y 活跃——因为 B2/B3 路径下 x 都会被覆盖。寄存器分配时,x 在 B1 之后的寄存器可以立即释放。

4.5 实例三:可用表达式分析(Available Expressions)

问题:在 p 点,表达式 a+b 是否已经在所有路径上计算过且操作数未变?

方向:  前向
交汇:  ∩(必须所有路径都计算过才算"可用")
gen:   块内计算的且之后操作数未被定值的表达式
kill:  操作数被定值的所有表达式
初始化: out[ENTRY] = ∅(入口处没有可用表达式)

用途:全局公共子表达式消除(GCSE)——如果 a+b 可用,直接用上次的结果。

注意 ∧ 从 ∪ 变成了 ∩ ——这改变了格的方向!对于可用表达式,"什么都不知道"是全 ∅(顶),"一切都确定"是全 U(底)。交汇用 ∩ 意味着越往下走信息越精确(越接近底)。


5. 支配分析(第 4 章)—— CFG 的结构骨架

5.1 支配关系
d 支配 n (d dom n): 从入口到达 n 的**所有路径**都经过 d
                    每个节点支配它自己(反身性)
d 严格支配 n:        d dom n 且 d ≠ n

立即支配者 idom(n): 严格支配 n 的节点中,"最接近 n"的
                    等价于: idom(n) 是支配树中 n 的父节点

手算支配集(还是上面的 CFG):

CFG:
        ENTRY
          │
         B1
        /  \
      B2    B3
        \  /
         B4
          │
        EXIT

支配关系:
  dom(B1)  = {ENTRY, B1}
  dom(B2)  = {ENTRY, B1, B2}
  dom(B3)  = {ENTRY, B1, B3}
  dom(B4)  = {ENTRY, B1, B4}          ← 所有路径从 B1 来,但 B2/B3 不是必经
  dom(EXIT) = {ENTRY, B1, B4, EXIT}

立即支配者:
  idom(B1)  = ENTRY
  idom(B2)  = B1
  idom(B3)  = B1
  idom(B4)  = B1                     ← 注意!B4 由 B1 支配,不是 B2/B3
  idom(EXIT) = B4
5.2 支配树

idom 关系画成树:

         ENTRY
           │
          B1
        /  |  \
      B2   B3  B4    ← B4 也是 B1 的子节点!即使它在 CFG 中是 B2/B3 的后继
                │
              EXIT

CFG 中的边(B2→B4、B3→B4)在支配树中不可见。支配树展示的是必经关系,不是控制流。

5.3 支配边界(Dominance Frontier)—— 核心概念
DF(n) = { x | n 支配 x 的某个前驱,但 n 不严格支配 x }

换成人话:从 n 的"势力范围"走到"势力范围外"的第一站。具体来说,x 在 DF(n) 中当且仅当:存在一条边 y→x,使得 n dom y 但 n 不 sdom x。

为什么支配边界重要?

支配边界 = 放 φ 节点的确切位置

SSA 构造算法的核心步骤(mem2reg Pass):

1. 对每个变量 x,找出所有给 x 赋值的块
2. 对每个赋值块 B,在 DF[B] 的每个块中插入 x 的 φ 节点
3. 新插入的 φ 节点本身也是赋值,所以递归应用步骤 2
4. 迭代直到不再新增 φ 节点
5. 用 φ 节点的参数重命名变量的所有使用

手算支配边界(同 CFG):

DF(B1)  = ∅              ← B1 支配所有节点,没有"出走"的边
DF(B2)  = {B4}           ← B2 dom B2 但 B2 不 sdom B4,边 B2→B4 的终点在 DF[B2]
DF(B3)  = {B4}           ← 同理,边 B3→B4
DF(B4)  = ∅              ← B4 的后继 EXIT 被 B4 严格支配

验证 φ 节点放置规则:假设 x 在 B2 中定值了一次,在 B3 中定值了一次。两个定值块都是"只在某些路径到达汇合点"的情况:

  • DF[B2] = {B4},所以 B4 开头要放 x = φ(x_B2, x_B1)
  • DF[B3] = {B4},B4 已经有 φ 了,不重复放
  • φ 节点本身在 B4 中是一个"新定值",但 DF[B4] = ∅,不需要继续传播
5.4 LLVM 中的支配信息
# 看 LLVM 怎么算支配树
clang -S -emit-llvm -O0 test.c -o test.ll
opt -passes='print<domtree>' test.ll -disable-output 2>&1

# 输出示例:
# [domtree] DominatorTree for function 'foo':
#   [1] %entry {0,7}          ← 入口
#     [2] %loop_header {1,6}  ← 父节点是 entry
#       [3] %body {2,3}
#       [4] %latch {2,5}

6. 循环识别(第 5 章)—— 不是所有环都是循环

6.1 自然循环的定义
回边 (back edge): 边 n → d,其中 d dom n(d 支配 n → 循环头支配循环体尾 → 这确实是往回跳)

自然循环 (natural loop): 回边 n → d 确定了一个循环
  - 循环头 = d
  - 循环体 = { d } ∪ { x | 存在路径 x→*→n 不经过 d }
    简单说:不经过 d 就能到达 n 的所有节点,加上 d 本身

实例

       [entry]
          │
          ▼
       [B1]  ←──┐    回边: B3 → B1
          │      │    dom(B1) = {entry, B1}, dom(B3) = {entry, B1, B2, B3}
          ▼      │    B1 dom B3 ✓ → 这是回边
       [B2]     │
          │      │    自然循环:
          ▼      │    循环头 = B1
       [B3] ────┘    循环体 = {B1, B2, B3}
          │            (B2 和 B3 都可以不经过 B1 到达 B3)
          ▼
       [exit]

不是循环的环(irreducible graph):

       [B1]
       /  \
      ▼    ▼
    [B2]  [B3]
      \    /
       ▼  ▼
       [B4]

如果边 B4→B1 存在但不是回边(B1 不支配 B4),这仍然是一个不可约控制流。现代编译器通常对此做节点分裂转成可约形式。

6.2 LLVM 的循环规范化

LLVM 的 LoopSimplify Pass 保证每个循环都有标准形态:

     [preheader]        ← 唯一入口(循环外的块)
         │                preheader 不属于循环体
         ▼
     [header]           ← 循环头(包含 phi 节点)
      /    \
     ▼      ▼
   [body] [latch]       ← latch:唯一回边源
      \    /
       ▼  ▼
     [header]           ← 循环头 φ 节点汇合 body 和 latch 的值
         │
         ▼
      [exit]            ← 唯一出口

为什么需要 preheader? 循环不变量外提(LICM)的代码就移到 preheader 中——它在循环外只执行一次。


7. 今天的动手任务

任务 1:手写一个微型 Parser(30 分钟)

用 Python 实现一个支持 + - * / ( ) 和变量赋值的递归下降 Parser,输出三地址码。这是理解"语法制导翻译"最直接的方式。

▼ 任务 1 参考实现
# mini_compiler.py —— 完整的表达式+赋值 Parser → 三地址码
# 支持: + - * / ( )  和  x = expr;

class MiniCompiler:
    def __init__(self, tokens):
        self.tokens = tokens
        self.pos = 0
        self.code = []       # 三地址码输出
        self.temp_n = 0

    def new_temp(self):
        self.temp_n += 1
        return f't{self.temp_n}'

    def emit(self, instr):
        self.code.append(instr)

    # --- lexer helpers ---
    def peek(self):
        return self.tokens[self.pos] if self.pos < len(self.tokens) else ('EOF', '')

    def consume(self):
        tok = self.peek()
        self.pos += 1
        return tok

    def expect(self, t):
        tok = self.consume()
        if tok[0] != t:
            raise SyntaxError(f"期望 {t},遇到 {tok}")
        return tok

    # --- grammar: stmt → IDENT = expr ; ---
    def parse_stmt(self):
        if self.peek()[0] == 'IDENT':
            name = self.consume()[1]
            self.expect('ASSIGN')
            val = self.parse_expr()
            self.expect('SEMI')
            self.emit(f'{name} = {val}')
            return None
        else:
            val = self.parse_expr()
            return val  # 允许裸表达式(交互式计算器)

    # --- grammar: expr → term expr_tail ---
    def parse_expr(self):
        left = self.parse_term()
        return self.parse_expr_tail(left)

    def parse_expr_tail(self, left):
        t = self.peek()[0]
        if t == 'PLUS':
            self.consume()
            right = self.parse_term()
            temp = self.new_temp()
            self.emit(f'{temp} = {left} + {right}')
            return self.parse_expr_tail(temp)
        elif t == 'MINUS':
            self.consume()
            right = self.parse_term()
            temp = self.new_temp()
            self.emit(f'{temp} = {left} - {right}')
            return self.parse_expr_tail(temp)
        return left

    # --- grammar: term → factor term_tail ---
    def parse_term(self):
        left = self.parse_factor()
        return self.parse_term_tail(left)

    def parse_term_tail(self, left):
        t = self.peek()[0]
        if t == 'STAR':
            self.consume()
            right = self.parse_factor()
            temp = self.new_temp()
            self.emit(f'{temp} = {left} * {right}')
            return self.parse_term_tail(temp)
        elif t == 'SLASH':
            self.consume()
            right = self.parse_factor()
            temp = self.new_temp()
            self.emit(f'{temp} = {left} / {right}')
            return self.parse_term_tail(temp)
        return left

    # --- grammar: factor → ( expr ) | NUMBER | IDENT ---
    def parse_factor(self):
        tok = self.peek()
        if tok[0] == 'NUMBER':
            self.consume()
            return tok[1]                    # 常量值直接返回
        elif tok[0] == 'IDENT':
            self.consume()
            return tok[1]                    # 变量名直接返回
        elif tok[0] == 'LPAREN':
            self.consume()
            val = self.parse_expr()
            self.expect('RPAREN')
            return val
        else:
            raise SyntaxError(f"意外的 Token: {tok}")


# ============ 测试 ============

def tokenize(s):
    """简易词法分析器:字符串 → Token 流"""
    import re
    rules = [
        ('NUMBER', r'\d+'),
        ('IDENT',  r'[a-zA-Z_]\w*'),
        ('ASSIGN', r'='),
        ('PLUS',   r'\+'),
        ('MINUS',  r'-'),
        ('STAR',   r'\*'),
        ('SLASH',  r'/'),
        ('LPAREN', r'\('),
        ('RPAREN', r'\)'),
        ('SEMI',   r';'),
    ]
    tokens = []
    pos = 0
    while pos < len(s):
        if s[pos].isspace():
            pos += 1
            continue
        matched = False
        for ttype, pattern in rules:
            m = re.match(pattern, s[pos:])
            if m:
                tokens.append((ttype, m.group()))
                pos += len(m.group())
                matched = True
                break
        if not matched:
            raise SyntaxError(f"无法识别的字符: '{s[pos]}'")
    return tokens


# 测试 1:表达式
print("=== 测试 1: 3 + 4 * 5 ===")
tokens = tokenize("3 + 4 * 5")
c = MiniCompiler(tokens)
result = c.parse_stmt()
for line in c.code:
    print(line)
# 期望输出:
# t1 = 4 * 5
# t2 = 3 + t1

# 测试 2:赋值
print("\n=== 测试 2: x = (a + b) * c ===")
tokens = tokenize("x = ( a + b ) * c ;")
c = MiniCompiler(tokens)
c.parse_stmt()
for line in c.code:
    print(line)
# 期望输出:
# t1 = a + b
# t2 = t1 * c
# x = t2

# 测试 3:带减法和除法的表达式
print("\n=== 测试 3: 10 - 8 / 2 ===")
tokens = tokenize("10 - 8 / 2")
c = MiniCompiler(tokens)
c.parse_stmt()
for line in c.code:
    print(line)
# 期望输出:
# t1 = 8 / 2
# t2 = 10 - t1

运行结果

=== 测试 1: 3 + 4 * 5 ===
t1 = 4 * 5
t2 = 3 + t1

=== 测试 2: x = (a + b) * c ===
t1 = a + b
t2 = t1 * c
x = t2

=== 测试 3: 10 - 8 / 2 ===
t1 = 8 / 2
t2 = 10 - t1

关键观察:三地址码 t1 = 4 * 5 先生成,t2 = 3 + t1 后生成——这自然保证了运算优先级(乘除先于加减)。因为 Parser 递归时 parse_term() 先于 parse_expr_tail() 被调用,所以乘法子树先被完整翻译。


任务 2:数据流分析手算(20 分钟)

对下面的 CFG,分别做到达定值活跃变量的迭代求解,逐遍记录 in/out。

        B1: a=1; b=2
        /       \
      B2: c=a+b   B3: b=3
        \       /
        B4: d=a+b
▼ 任务 2 参考答案

CFG 标注定义语句

        [B1]
    d1: a = 1
    d2: b = 2
       /     \
      ▼       ▼
    [B2]      [B3]
 d3: c = a+b  d4: b = 3
      \       /
       ▼     ▼
        [B4]
    d5: d = a+b

A. 到达定值分析(Reaching Definitions)

方向: 前向
交汇: ∪
gen/kill:
  gen[B1] = {d1, d2}
  kill[B1] = {d1, d2 以及任何对 a 或 b 的其他定义} → 对全局就是 {其他 a 或 b 的定义}

  gen[B2] = {d3}
  kill[B2] = {}    (d3 定义了新变量 c,没杀任何已有定义)

  gen[B3] = {d4}
  kill[B3] = {d2}  (d4 定义 b,杀了 d2 对 b 的定义)

  gen[B4] = {d5}
  kill[B4] = {}

迭代求解:

Pass 1:
  out[B1] = gen[B1] = {d1, d2}

  in[B2]  = out[B1] = {d1, d2}
  out[B2] = gen[B2] ∪ (in[B2] - kill[B2])
          = {d3} ∪ ({d1, d2} - ∅)
          = {d1, d2, d3}

  in[B3]  = out[B1] = {d1, d2}
  out[B3] = gen[B3] ∪ (in[B3] - kill[B3])
          = {d4} ∪ ({d1, d2} - {d2})
          = {d1, d4}

  in[B4]  = out[B2] ∪ out[B3]
          = {d1, d2, d3} ∪ {d1, d4}
          = {d1, d2, d3, d4}
  out[B4] = gen[B4] ∪ (in[B4] - kill[B4])
          = {d5} ∪ ({d1, d2, d3, d4} - ∅)
          = {d1, d2, d3, d4, d5}

Pass 2:
  in[B2]  = out[B1] = {d1, d2}  (不变)
  in[B3]  = out[B1] = {d1, d2}  (不变)
  in[B4]  = out[B2] ∪ out[B3] = {d1,d2,d3,d4} (不变)
  → 所有 in/out 不变 → 终止

结果解读:
  在 B4 的入口:
    a 的定义 → {d1} (只有一处定义 a)
    b 的定义 → {d2, d4} (b 可能来自 B1 的 2 或 B3 的 3)
    c 的定义 → {d3} (只有 if 分支定义)
  → 所以 d = a + b 在 B4 中,a 固定是 1,但 b 可能是 2 或 3
  → 编译器不能在这做常量折叠

B. 活跃变量分析(Live Variables)

方向: 后向
交汇: ∪
def/use:
  def[B1] = {a, b}    use[B1] = ∅
  def[B2] = {c}       use[B2] = {a, b}
  def[B3] = {b}       use[B3] = ∅
  def[B4] = {d}       use[B4] = {a, b}

初始化: in[EXIT] = ∅  (EXIT 是 B4 后面)

Pass 1 (逆序: B4 → B3 → B2 → B1):
  out[B4] = ∅
  in[B4]  = use[B4] ∪ (out[B4] - def[B4])
          = {a, b} ∪ (∅ - {d}) = {a, b}
  → a 和 b 在 B4 入口是活跃的(它们在 B4 中被用了)

  out[B3] = in[B4] = {a, b}
  in[B3]  = use[B3] ∪ (out[B3] - def[B3])
          = ∅ ∪ ({a, b} - {b}) = {a}
  → a 从 B3 入口活跃(穿过了 B3,b 在 B3 被重新定义了)

  out[B2] = in[B4] = {a, b}
  in[B2]  = use[B2] ∪ (out[B2] - def[B2])
          = {a, b} ∪ ({a, b} - {c}) = {a, b}
  → a, b 都从 B2 入口活跃

  out[B1] = in[B2] ∪ in[B3] = {a, b} ∪ {a} = {a, b}
  in[B1]  = use[B1] ∪ (out[B1] - def[B1])
          = ∅ ∪ ({a, b} - {a, b}) = ∅
  → 函数入口处没有活跃变量(合理:参数 cond 没用上)

Pass 2: 所有 in/out 不变 → 终止

结果解读:
  在 B1 出口: a 和 b 都活跃 → 它们的寄存器不能释放
  在 B3 出口: 只有 a 活跃 → b 在 B3 被重新定义,原来的 b 可以释放
  在 B4 出口: 没有活跃变量(除了返回值 d)

任务 3:支配关系手算(10 分钟)

对任务 2 的 CFG 算支配关系、支配树、支配边界。验证:如果在 B2 和 B3 中分别给同一个变量赋值,φ 节点应该放在哪个块?

▼ 任务 3 参考答案

CFG:

        [ENTRY]
           │
          [B1]
         /    \
       [B2]   [B3]
         \    /
          [B4]
           │
         [EXIT]

支配关系(dom):

从 ENTRY 到每个节点的所有路径:

  • 到 B1: ENTRY → B1,所有路径必过 ENTRY 和 B1 → dom(B1) = {ENTRY, B1}
  • 到 B2: ENTRY → B1 → B2,所有路径必过 ENTRY、B1、B2 → dom(B2) = {ENTRY, B1, B2}
  • 到 B3: ENTRY → B1 → B3,所有路径必过 ENTRY、B1、B3 → dom(B3) = {ENTRY, B1, B3}
  • 到 B4: 有两条路径 (B1→B2→B4) 和 (B1→B3→B4)。两条路径都经过 ENTRY 和 B1,但不都经过 B2 或 B3 → dom(B4) = {ENTRY, B1, B4}
  • 到 EXIT: 所有路径必过 ENTRY、B1、B4、EXIT → dom(EXIT) = {ENTRY, B1, B4, EXIT}

立即支配者(idom):

idom(B1)  = ENTRY    (ENTRY 是唯一严格支配 B1 的节点)
idom(B2)  = B1       (严格支配 B2 的有 ENTRY, B1,B1 是最近的)
idom(B3)  = B1       (同理)
idom(B4)  = B1       (严格支配 B4 的有 ENTRY, B1,B1 是最近的)
idom(EXIT) = B4      (严格支配 EXIT 的有 ENTRY, B1, B4,B4 最近)

支配树:

         ENTRY
           │
          B1
        /  |  \
      B2   B3  B4
                │
              EXIT

注意:B4 在支配树中直接挂在 B1 下面,而不是 B2 或 B3。支配树不等于 CFG!

支配边界(DF):

DF(ENTRY) = ∅        ; ENTRY 支配所有节点,没有任何边"逃出"它的势力范围
DF(B1)   = ∅         ; B1 的每个后继 (B2, B3) 都被 B1 严格支配,没有"逃出"的边
                       B4 也被 B1 严格支配

DF(B2)   = {B4}      ; B2→B4 是一条边,B2 dom B2 但 B2 不 sdom B4
DF(B3)   = {B4}      ; B3→B4 是一条边,同理
DF(B4)   = {EXIT}    ; B4→EXIT 是一条边,B4 dom B4 但 B4 不 sdom EXIT(?) 
                       不对——B4 sdom EXIT,所以 B4→EXIT 不产生支配边界
                       修正: DF(B4) = ∅
DF(EXIT) = ∅         ; EXIT 没有后继

φ 节点放置验证:

假设变量 x 在 B2 中赋值(x = ...),也在 B3 中赋值:

x 在 B2 中赋值 → DF[B2] = {B4} → 在 B4 开头插入 φ
x 在 B3 中赋值 → DF[B3] = {B4} → B4 已有 φ,不重复

B4 中的 φ: %x = phi [x_from_B2, %B2], [x_from_B3, %B3]

答案:φ 节点放在 B4

这个结果很直观——B4 是两条路径的汇合点,变量在两条路径上分别被赋值,所以在 B4 需要用 φ 来汇合。

LLVM 验证

cat > phi_test.c << 'EOF'
int foo(int cond, int a, int b) {
    int x;
    if (cond) {
        x = a + 1;     // B2
    } else {
        x = b - 1;     // B3
    }
    return x;           // B4
}
EOF

clang -S -emit-llvm -O0 phi_test.c -o phi_test.ll
opt -passes=mem2reg phi_test.ll -S | grep -A5 "phi"

你会发现 φ 节点确实在汇合块(if.end 或类似标签)中。


任务 4:用 LLVM 验证(15 分钟)
# 把任务 2 的 CFG 写成 C 代码
cat > test.c << 'EOF'
int foo(int cond) {
    int a = 1, b = 2, c, d;
    if (cond) {
        c = a + b;
    } else {
        b = 3;
    }
    d = a + b;
    return d;
}
EOF

clang -S -emit-llvm -O0 test.c -o test.ll
# 手工找出 IR 中的基本块,标出 phi 节点

opt -passes='print<domtree>' test.ll -disable-output 2>&1
# 对比自己算的支配树

opt -passes='mem2reg,print<domtree>' test.ll -disable-output 2>&1
# 观察 mem2reg 怎么插入 phi 节点
▼ 任务 4 参考答案

Step 1: 生成的 -O0 IR(精简)

define i32 @foo(i32 %cond) {
entry:
  %a = alloca i32
  %b = alloca i32
  %c = alloca i32
  %d = alloca i32
  store i32 1, ptr %a          ; a = 1
  store i32 2, ptr %b          ; b = 2
  %tobool = icmp ne i32 %cond, 0
  br i1 %tobool, label %if.then, label %if.else

if.then:
  %0 = load i32, ptr %a
  %1 = load i32, ptr %b
  %add = add nsw i32 %0, %1    ; c = a + b
  store i32 %add, ptr %c
  br label %if.end

if.else:
  store i32 3, ptr %b          ; b = 3
  br label %if.end

if.end:
  %2 = load i32, ptr %a
  %3 = load i32, ptr %b
  %add1 = add nsw i32 %2, %3   ; d = a + b
  store i32 %add1, ptr %d
  ret i32 %add1                ; return d
}

基本块识别entryif.thenif.elseif.end(4 个块)。和我们任务 2 的 CFG 一一对应。

Step 2: mem2reg 后

define i32 @foo(i32 %cond) {
entry:
  %tobool = icmp ne i32 %cond, 0
  br i1 %tobool, label %if.then, label %if.else

if.then:
  %add = add nsw i32 1, 2       ; a=1, b=2 常量:c = 1+2 = 3
  br label %if.end

if.else:
  br label %if.end

if.end:
  %b.0 = phi i32 [ 2, %if.then ], [ 3, %if.else ]   ; ← φ 节点!b 来自 2 或 3
  %add1 = add nsw i32 1, %b.0                         ; d = 1 + b
  ; 注意 a=1 也被常量折叠了
  ret i32 %add1
}

对比我们手算的结果

  • φ 节点插在 if.end(= B4)——正好是我们任务 3 算出的支配边界!
  • if.then 路径 → b = 2(原始值没被改)
  • if.else 路径 → b = 3(else 中改了)
  • a = 1 是常量,被折叠了(LLVM 将 a+b 变成了 1 + %b.0

Step 3: 支配树输出

opt -passes='print<domtree>' test.ll -disable-output 2>&1

输出(精简):

[domtree] DominatorTree for function 'foo':
  [1] %entry {0,3}
    [2] %if.then {1,2}
    [2] %if.else {1,2}
    [2] %if.end {1,2}

解读:

  • [1] 表示 entry 是根(深度 1)
  • [2] 表示 if.then / if.else / if.end 都是深度 2,父节点都是 entry
  • 这和我们手算的 idom 一致——if.end 的立即支配者是 entry(经过 mem2reg 后,entry 中的 store 被清空,但支配关系不变)

Step 4: 最终 O2 输出

clang -S -emit-llvm -O2 test.c -o test_O2.ll
define i32 @foo(i32 %cond) {
entry:
  %tobool.not = icmp eq i32 %cond, 0          ; cond == 0 ?
  %. = select i1 %tobool.not, i32 3, i32 2    ; cond ? 2 : 3(倒过来了因为 icmp eq)
  %add1 = add nsw i32 %., 1                   ; d = (cond?2:3) + 1
  ret i32 %add1
}

O2 干的事:

  1. b 的两条路径值 23 合并为一个 select
  2. a=1 内联为常量 1
  3. 消除了所有基本块——整个函数变成一个基本块
  4. if.then 中的 c = a+b = 1+2 = 3 被常量折叠,但 c 没被返回 → 死代码消除删了它
  5. 这个函数体现的是:常量传播 + mem2reg + simplifycfg 的协同效果

今日小结

上午(龙书理论):
  词法分析: 正则 → NFA → DFA → 最小化 DFA
  语法分析: 递归下降(LL) vs LR(0)→SLR→LR(1)→LALR(1)
  语法制导: 综合属性 ↑ / 继承属性 ↓ / SDT 动作嵌入

下午(LLVM 分析基础):
  数据流分析: in[B] = ∧out[前驱]; out[B] = gen ∪ (in - kill)
    三种实例: 到达定值(前向∪) / 活跃变量(后向∪) / 可用表达式(前向∩)
  支配分析: dom / idom / 支配树 / 支配边界
    支配边界 = φ 节点放置位置
  循环识别: 回边 / 自然循环 / 循环规范化(preheader→header→body/latch→exit)

关键认知

  1. 数据流分析是统一的:所有优化问题都在同一个 in/out/gen/kill/∧ 模板里,差异只是方向(前/后向)和交汇(∪/∩)。Day3 的所有 LLVM Pass 都是这个模板的特化。

  2. 支配边界直接解释 SSA:为什么 alloca + store + load 经过 mem2reg 就变成 SSA?因为支配边界精确标定了 φ 节点的位置。这不是魔术,是可以在纸上推导的算法。

  3. 循环规范化是所有循环优化的前提:LICM、循环展开、向量化——LLVM 的每个循环 Pass 都假设 preheader 存在、latch 唯一。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值