重新理解编译器

重新理解编译器

2024 南京大学《操作系统:设计与实现》
重新理解编译器

什么是编译器?

编译器的输入

  • 高级语言 (C) 代码 = 状态机

编译器的输出

  • 汇编代码 (指令序列) = 状态机

编译器 = 状态机之间的翻译器

2024 南京大学《操作系统:设计与实现》
重新理解编译器

SimpleC: 直接翻译

运算

  • 把操作数 load 到寄存器、执行运算、store 写回结果

分支/循环

  • 使用条件跳转分别执行代码

函数调用

  • 专门留一个寄存器给栈 (SP, Stack Pointer)
  • 将 Stack frame 的信息保存在内存里
    • 通过 SP 可以访问当前栈帧的变量、返回地址等

(SimpleC 编译器很适合作为《计算机系统基础》的编程练习)

2024 南京大学《操作系统:设计与实现》
重新理解编译器

SimpleC: 直接翻译 (cont'd)

所以,C 被称为高级汇编语言

  • 存在 C 代码到指令集的直接对应关系
    • 状态机和迁移都可以 “直译”
    • 于是计算机系统里多了一个抽象层 (“一生二、二生三、三生万物”)
  • 更 “高级” 的语言就很难了
    • C++ virtual void foo();
    • Python [1, 2, 3, *rest]
    • Javascript await fetch(...)

C 语言能实现对机器更好的控制 (例子:Inline Assembly)

2024 南京大学《操作系统:设计与实现》
重新理解编译器

编译优化 🌶️🌶️

C 语言编译器在进行代码优化时,遵循的基本准则是在不改变程序的语义 (即程序的行为和输出结果) 的前提下,提高程序的执行效率和/或减少程序的资源消耗

int foo(int x) {
    int y = x + 1;
    return y - 1;
}

一些 “不改变语义” 的例子 (编译优化中最重要的 “三板斧”):

  • 函数内联:将函数调用替换为函数体本身的内容
  • 常量传播:在编译时计算常量表达式的值并替换
  • 死代码消除:删除永远不会被执行到的代码
2024 南京大学《操作系统:设计与实现》
重新理解编译器

但如果我们问一个更本质的问题…… 🌶️🌶️

给两个程序 A,BA, B,编译器到底允许不允许把 AA 编译成 BB

  • 刚才的优化怎么就 “不改变语义” 了?
  • 什么才算 “改变语义”?

考虑一个特殊情况

  • 一个程序没有任何系统调用
    • 它甚至不能终止
      • main 函数直接返回是 undefined behavior
    • 无论它有多少代码
      • 优化成 while (1) 就好了
2024 南京大学《操作系统:设计与实现》
重新理解编译器

编译正确性 🌶️🌶️

系统调用是使程序计算结果可见的唯一方法

  • 不改变语义 = 不改变可见结果
  • 状态机的视角:满足C/汇编状态机生成的所有 syscall 序列完全一致,任何优化都是允许的

C 代码中的不可优化部分

  • External function calls (链接时才能确定到底是什么代码)
    • 未知的代码可能包含系统调用
    • 因此不可删除、移出循环等,且要保证参数传递完全一致
  • 编译器提供的 “不可优化” 标注
    • volatile [load | store | inline assembly]
2024 南京大学《操作系统:设计与实现》
重新理解编译器

有没有觉得这个定义保守了?🌶️🌶️🌶️

凭什么系统调用不能被优化?

if (n <= 26) {
    for (int i = 0; i < n; i++) {
        putchar('A' + i);
    }
}
  • 凭什么不能合并成一个 printf

把状态机的一部分直接放到操作系统里运行

  • 把代码放进操作系统运行:XRP
  • 单个应用就是操作系统:Unikernel
2024 南京大学《操作系统:设计与实现》