English
A compiler translates a program into another representation while preserving the behavior required by the source language. The target may be machine code, bytecode, another language, or an intermediate representation. For total = price * count, translation must determine what the names mean, which arithmetic rules apply, and how to implement the operation on the target. Parsing the expression is only the first part of that work.
A runtime supplies facilities used during execution: allocation, exception handling, dynamic dispatch, garbage collection, scheduling, or foreign-function interfaces, depending on the implementation. An interpreter is also an execution mechanism. Languages do not each have one fixed compile-or-interpret identity, and a statically typed language can use a substantial runtime. Distinguish the language specification from a particular implementation.
These two subjects appear together because the boundary between them has eroded. Modern optimizing compilers generate code that is only correct in concert with specific runtime support — garbage collection, virtual dispatch tables, exception handling infrastructure. JIT compilers are simultaneously compiler and runtime, translating code during execution and deoptimizing when speculative assumptions prove wrong. Profile-guided optimization feeds runtime measurements back into the static compiler, blurring the batch-process model of traditional compilation. The toolchain-and-runtime system as a whole is the meaningful unit, not either component separately.
Prerequisites: Programming language theory (§3.4) — semantics, type systems, and the formal concepts that define what compilers must preserve. Computer organization and architecture (§4.1) — the target side of compilation; what registers, pipelines, and caches mean for code generation. Operating systems (§4.2) — the environment in which runtimes operate; memory management, process model, dynamic linking.
From Translation to Systems: How Compiler Engineering Matured
Early automatic programming addressed two costs: programmers’ time and machines’ time. Hopper’s A-0 system (1952) assembled calls to stored routines; describing it simply as a modern high-level-language compiler hides the difference. The FORTRAN team led by Backus delivered its IBM 704 compiler in 1957, investing heavily in code quality so mathematical notation would be practical on expensive machines. Haigh and Ceruzzi’s history emphasizes programming and debugging costs as well as execution speed. Successful abstraction made the whole task cheaper, not just the source shorter.
Formal grammars helped specify language syntax and build parsers. ALGOL’s use of a formal syntax notation and work on context-free parsing made language descriptions more precise. Lexer and parser generators, including Lex and Yacc, automated parts of front-end construction. A grammar still does not define name binding, types, evaluation, or useful error messages. Parsing is a well-developed area, not the whole problem of understanding a program.
Lisp’s early garbage collection connected language implementation to automatic storage management. Tracing from accessible roots lets a runtime reclaim unreachable objects without asking the programmer to free each one. Later generational, incremental, and concurrent techniques explored different costs. C++ resource management and Rust ownership explored other ways to control lifetimes; they should not be described as eliminating every leak or every runtime allocation cost.
Optimization was already central to FORTRAN. Subsequent work developed control-flow graphs, basic blocks, dominance, and dataflow analysis. These representations let a compiler reason about which values can reach a use, which expressions remain unchanged, and which operations are unnecessary. Analysis establishes the conditions under which a transformation is legal; a cost model then estimates whether it is worthwhile.
Work on static single assignment (SSA), including the 1991 paper by Cytron and colleagues on efficient construction, made def-use relationships easier to represent. Each SSA value has one definition, and merge operations select the appropriate incoming value at a control-flow join. This simplifies many analyses but does not make optimization trivial: loops, memory aliasing, side effects, and interacting passes still require reasoning.
LLVM developed reusable optimization and code-generation infrastructure around an explicit intermediate representation. It reduced the need for each language project to implement every backend component. Reuse still requires implementing source-language semantics, runtime support, debugging information, calling conventions, and appropriate lowering. Earlier reusable backends such as GCC also existed; LLVM did not invent the possibility of sharing compiler infrastructure.
Adaptive compilation developed through systems including Smalltalk, Self, and later Java’s HotSpot and JavaScript engines. A runtime can identify frequently executed code and compile a specialized version. For example, it may optimize a call that has so far seen one receiver type, while retaining a guard and a path back to more general execution. Profiling suggests a profitable assumption; the guard preserves correctness if that assumption stops holding. Compilation time and warm-up are part of the total cost.
Domain-specific compilation makes higher-level structure available to optimization. Halide exposes image-processing computation and scheduling; tensor compilers can fuse operators and choose layouts for accelerators. MLIR supports extensible dialects with different operations and types, allowing multiple levels of representation. Dialects need not form one fixed hierarchy. The recurring design question is when to lower an abstraction without losing information needed for later decisions.
Translation, Optimization, and Runtime Adaptation
Follow a small program through syntax, name and type analysis, intermediate representation, optimization, and execution. At each stage, ask what information is explicit, what behavior must be preserved, and which remaining decisions depend on the target or runtime.
The Intermediate Representation as Engineering Decision
Intermediate representations make particular questions easier to answer. An abstract syntax tree reflects source structure; a control-flow graph makes possible transfers of control explicit; SSA connects each value use to one definition. A backend selects instructions, allocates registers, and arranges target-specific code. Real compilers can revisit representations and perform several lowering stages rather than following one universal pipeline.
The expression 1 + 2 * 3 illustrates why source structure matters. A grammar that merely allows any expression to be joined to another with + or * permits both (1 + 2) * 3 and 1 + (2 * 3): the same token sequence has two parse trees. Giving multiplication higher precedence selects the second grouping, whose abstract syntax tree can be written Add(1, Mul(2, 3)). Evaluating the selected tree gives 7; the other gives 9. A compiler must settle this structure before optimizing it. Precedence fixes grouping; it does not, by itself, specify the order in which effectful operands are evaluated.
Consider if flag: x = 1; else: x = 2; return x + 3. SSA can give the branch definitions distinct names, x1 and x2, and select one at the join using x3 = phi(x1, x2). The choice depends on which predecessor was actually taken; phi does not average values or execute both branches. If analysis proves flag true, the result can become the constant four. Otherwise the two possible paths remain relevant.
LLVM IR represents explicit control flow, typed values, and memory operations. A source loop becomes blocks and branches; losing source syntax does not mean losing every analyzable loop structure. Memory is not automatically in SSA merely because register-like values are. Alias analysis still needs to determine whether different pointers can refer to overlapping storage. See the LLVM language reference for the actual semantics of instructions rather than inferring them from source-language notation.
Higher-level IR can retain array shapes, tensor operations, or structured loops for transformations that need them. Lower-level IR exposes address arithmetic and machine-oriented operations. MLIR’s dialect design supports representing these different concerns. Choosing an IR means choosing what information to preserve and which transformations to make convenient.
Optimization as Semantics-Preserving Transformation
An optimization must preserve the observable behavior required by the language and compilation options. Observable behavior can include output, exceptions, volatile accesses, and permitted concurrent interactions. For nondeterministic specifications, correctness need not mean reproducing every possible original execution, but it must stay within the permitted behavior and any required progress guarantees.
Constant propagation can replace a known value; dead code elimination can remove work with no required effect; inlining exposes a function body to analysis of its calling context. None is a purely textual substitution rule. An unused function result does not make the call removable if it writes a file. Moving an expression out of a loop may introduce an exception on a path where the expression previously never executed. Pointers can alias, so a store may invalidate an apparently unchanged load.
Arithmetic rules also matter. Integer overflow can wrap, trap, or be undefined, depending on the language and operation. Floating-point reassociation can change rounding. A valid transformation for mathematical integers is not automatically valid for every machine-number model. Optimization flags that permit relaxed arithmetic change what behavior the compiler is required to preserve.
Verification and testing provide different evidence. CompCert proves semantic preservation for its verified compilation pipeline under its formalized assumptions; this does not prove every surrounding tool or source program correct. Production compilers also use regression tests, differential testing, and targeted checking of transformations. When investigating a suspected miscompilation, first establish that the source program has defined behavior.
Profile-guided optimization uses observed workloads to inform inlining, layout, and other choices. A profile can guide profitability but cannot by itself justify an otherwise invalid transformation. Measure the resulting program on representative inputs, including startup and memory use where relevant; there is no universal percentage improvement.
Runtime Systems: Garbage Collection and Concurrency
Tracing garbage collection follows references from roots such as stacks and global variables. It reclaims objects found unreachable under its algorithm. Reachable does not mean useful: an obsolete object retained by a global cache can occupy memory indefinitely. Garbage collection therefore prevents some lifetime errors while still allowing retention problems.
Generational collectors exploit workloads in which many objects become unreachable soon after allocation. They need mechanisms, such as write barriers, to track relevant references across generations. Incremental collection splits work into pieces; concurrent collection overlaps work with application execution; parallel collection uses multiple collector threads. These are different properties. Pause time, total CPU work, extra memory, and allocation rate need to be assessed together.
Reference counting reclaims an object when its owning count reaches zero, but destruction can trigger a long cascade and cycles can keep counts nonzero. It is not automatically a bounded-latency solution. Rust’s ownership rules provide memory-safety guarantees but permit reference-counted cycles and leaks, as the Rust Book example shows. Weak references can break ownership cycles when that matches the intended lifetime model.
Runtime scheduling determines how logical tasks use operating-system threads. Go’s goroutines and Rust executors for futures make different choices about scheduling and suspension. Neither an async syntax marker nor a large task count creates additional CPU capacity. A blocking operation can occupy an executor thread and delay unrelated tasks unless the runtime or application provides an appropriate way to offload it.
What Studying This Changes
Compilers and runtimes change how practitioners understand and diagnose the gap between source code and execution.
Reading intermediate code and assembly helps test explanations of performance. Compile a small function, vary one condition, and inspect whether a loop was vectorized or a call inlined. The output shows what was emitted; optimization remarks and profiles help explain why and whether it matters. It does not reveal every runtime condition or make performance predictable from instruction count alone.
Runtime knowledge helps test causes of memory growth and latency. A retained-object graph can explain why a collector cannot reclaim an obsolete object; allocation and pause measurements can distinguish object churn from unrelated waiting. Change a runtime setting only after identifying the behavior it controls and measuring the result.
The same ideas help design a small configuration or query language. Define valid syntax, name lookup, types, evaluation, and error behavior before adding optimizations. A short language still needs a precise meaning, and the required runtime facilities follow from that meaning.
Resources
Books and Texts
Crafting Interpreters builds a tree-walking interpreter and a bytecode virtual machine. Engineering a Compiler develops analysis, optimization, and code generation; Appel’s Modern Compiler Implementation organizes a compiler project. The Dragon Book covers front ends as well as substantial optimization and code-generation material. The Garbage Collection Handbook studies memory-management algorithms, and Muchnick’s Advanced Compiler Design and Implementation provides detailed optimization treatments. Choose complementary chapters rather than ranking the entire books as interchangeable.
| Book | Role | Type |
|---|---|---|
| Nystrom, Crafting Interpreters (free) | Accessible entry with two working implementations | Practice |
| Cooper & Torczon, Engineering a Compiler (3rd ed.) | Compiler analysis, optimization, and code generation | Entry |
| Appel, Modern Compiler Implementation (ML/Java/C) | Complete-project alternative primary text | Practice |
| Aho, Lam, Sethi & Ullman, Compilers (Dragon Book) | Syntax, semantics, optimization, and code generation | Reference |
| Jones, Hosking & Moss, The Garbage Collection Handbook (2nd ed.) | GC algorithms reference | Reference |
| Muchnick, Advanced Compiler Design and Implementation | Optimization techniques at depth | Depth |
Courses and Lectures
Stanford CS 143 provides a compiler-construction path. Cornell CS 6120 studies IR, analysis, and optimization using Bril as well as LLVM; its exercises are not all LLVM passes. The course lessons show the progression. Nand to Tetris adds a small complete hardware-and-software stack, useful for locating the compiler within a working system.
| Course | Platform | Type |
|---|---|---|
| Stanford CS 143 Compilers (free) | Stanford / course site | Entry |
| Cornell CS 6120 Advanced Compilers (free) | cs.cornell.edu/courses/cs6120 | Depth |
Practice, Tools, and Current Sources
Use Compiler Explorer to compare source with generated output and LLVM Kaleidoscope to build a small frontend. Select documentation matching the LLVM version. When reading HotSpot, Go, or V8, follow one mechanism—an allocation path, scheduler decision, or guarded optimization—rather than trying to absorb the whole runtime. Compare source behavior with telemetry from an executable example.
| Resource | Platform | Type |
|---|---|---|
| Compiler Explorer (free) | godbolt.org | Practice |
| LLVM Kaleidoscope tutorial (free) | llvm.org/docs/tutorial | Practice |
| OpenJDK HotSpot source | github.com/openjdk/jdk | Reference |
| V8 source and blog | github.com/v8/v8, v8.dev/blog | Reference |
| Go runtime source | github.com/golang/go/tree/master/src/runtime | Reference |
Traps
| Trap | Why it misleads | Better response |
|---|---|---|
| Treating one textbook as the whole field | Parsing, semantics, optimization, code generation, and runtimes require different perspectives. | Use a primary text and supplement the topics your project needs; the Dragon Book also covers substantial optimization material. |
| Equating a parser with a compiler | Syntax recognition does not determine binding, types, evaluation, or target code. | Follow a small program through semantic analysis, IR, and execution. |
| Treating the runtime as irrelevant | Allocation, retained objects, scheduling, and JIT work can affect latency and throughput. | Inspect runtime telemetry and connect it to allocation and execution patterns. |
| Assuming a toy compiler covers production requirements | Debugging, diagnostics, ABI compatibility, recovery, and large codebases add requirements. | Extend one small feature with a stated semantic rule, then compare it with a production implementation. |
| Assuming JIT or AOT always wins | Startup, compilation cost, profiles, and steady-state execution favor different choices. | Measure the whole workload, including warm-up, memory, and the required predictability. |
| Lowering abstractions before using their information | Shapes, loops, and domain operations can enable transformations lost in lower-level code. | Compare what each IR preserves and why a lowering step is scheduled there. |
中文
编译器把程序翻译成另一种表示,同时保持源语言要求的行为。目标可以是机器码、字节码、另一种语言或中间表示。对于 total = price * count,翻译需要确定名称指什么、适用哪种算术规则,以及怎样在目标上实现操作。解析表达式只是其中的第一部分。
运行时提供执行期间使用的设施,依实现而定,可以包括分配、异常处理、动态分派、垃圾回收、调度或外部函数接口。解释器也是一种执行机制。每种语言并非天生只能编译或只能解释,静态类型语言也可以使用复杂运行时。需要区分语言规范与某一种实现。
这两个主题之所以放在一起,是因为二者之间的边界已经被侵蚀。现代 optimizing compilers 生成的代码,只有与特定运行时支持——garbage collection、virtual dispatch tables、exception handling infrastructure——配合时才是正确的。JIT compilers 同时是编译器和运行时:它们在执行期间翻译代码,并在 speculative assumptions 被证明错误时 deoptimize。Profile-guided optimization 会把运行时测量反馈给静态编译器,模糊了传统编译作为批处理过程的模型。真正有意义的单位,是 toolchain-and-runtime system 整体,而不是其中任一组件单独存在。
前置知识:编程语言理论(§3.4)——语义、类型系统,以及定义编译器必须保持什么的形式概念。计算机组成与体系结构(§4.1)——编译目标侧;寄存器、流水线和缓存对代码生成意味着什么。操作系统(§4.2)——运行时运行其中的环境;内存管理、进程模型、动态链接。
从翻译到系统:编译器工程如何成熟
早期自动编程同时处理两种成本:程序员的时间与机器的时间。Hopper 的 A-0 系统(1952)组织对已存储例程的调用;直接称它为现代意义的高级语言编译器,会掩盖其中差别。Backus 领导的 FORTRAN 团队在 1957 年交付 IBM 704 编译器,大力改进生成代码的质量,使数学记法能够在昂贵机器上实用。Haigh 与 Ceruzzi 的历史叙述既强调执行速度,也强调编程与调试成本。成功的抽象降低了整个任务的成本,而不只是缩短源代码。
形式文法帮助人们规定语言语法并构造解析器。ALGOL 使用形式语法记法,以及上下文无关解析的研究,使语言描述更加精确。Lex、Yacc 等词法与语法分析器生成工具,自动化了部分前端构造。但文法仍不定义名称绑定、类型、求值或有用的错误信息。解析是一个发展成熟的领域,并不等于理解程序的全部问题。
Lisp 的早期垃圾回收,把语言实现与自动存储管理连接起来。从可访问的根出发追踪引用,使运行时不必要求程序员逐个释放对象,就能回收不可达对象。后来的分代、增量与并发技术探索不同成本。C++ 的资源管理与 Rust 所有权探索了其他生命周期控制方式,不能把它们说成消除了所有泄漏或所有运行时分配成本。
优化在 FORTRAN 中已经十分重要。后续研究发展了控制流图、基本块、支配关系与数据流分析。这些表示使编译器能够推理:哪些值可以到达某次使用,哪些表达式保持不变,以及哪些操作不再需要。分析建立变换合法的条件,成本模型再估计变换是否值得。
静态单赋值(SSA)的研究,包括 Cytron 等人 1991 年关于高效构造的论文,使定义与使用关系更易表示。每个 SSA 值只有一个定义,合并操作在控制流汇合处选择相应的传入值。这简化了许多分析,但不使优化变得简单:循环、内存别名、副作用与相互影响的优化阶段仍需要推理。
LLVM 围绕明确的中间表示,发展可复用的优化与代码生成基础设施,减少了每个语言项目从头实现所有后端组件的需要。但复用仍要实现源语言语义、运行时支持、调试信息、调用约定与合适的降级转换。GCC 等更早的可复用后端也已经存在;共享编译器基础设施并非始于 LLVM。
自适应编译经过 Smalltalk、Self,以及后来的 Java HotSpot、JavaScript 引擎等系统发展起来。运行时可以识别经常执行的代码,并编译其专用版本。例如,可以针对目前只出现过一种接收者类型的调用优化,同时保留检查与退回通用执行的路径。性能剖析提示某个假设可能有利,检查则在假设不再成立时保持正确性。编译时间与预热也是总成本的一部分。
领域专用编译让优化能够利用更高层结构。Halide 显式表达图像处理计算与调度;张量编译器可以融合算子,并为加速器选择布局。MLIR 支持具有不同操作和类型的可扩展方言,允许同时使用多个表示层次。方言不必形成唯一固定的层级。一个反复出现的设计问题是:何时降低抽象层次,才能避免丢失后续决策所需的信息?
翻译、优化与运行时适应
跟踪一个小程序经过语法、名称与类型分析、中间表示、优化和执行。在每一步询问:哪些信息已经明确,哪些行为必须保持,以及哪些未决事项依赖目标机器或运行时。
Intermediate Representation 作为工程决策
中间表示使某类问题更容易回答。抽象语法树反映源结构,控制流图明确可能的控制转移,SSA 把每次值的使用连接到唯一的定义。后端选择指令、分配寄存器并安排目标相关代码。真实编译器可以重复处理表示、经过多次降级转换,而非遵循唯一通用流水线。
表达式 1 + 2 * 3 能说明源结构为什么重要。如果文法只是允许用 + 或 * 连接任意两个表达式,就同时允许 (1 + 2) * 3 和 1 + (2 * 3):同一串词法记号有两棵分析树。规定乘法优先级更高,就选定了后一种分组,其抽象语法树可以写成 Add(1, Mul(2, 3))。对这棵树求值得到 7,对另一棵则得到 9。编译器必须先确定结构,才能进行优化。优先级确定分组方式,本身并不规定带副作用的操作数按什么顺序求值。
考虑 if flag: x = 1; else: x = 2; return x + 3。SSA 可以把分支定义分别命名为 x1、x2,再在汇合处用 x3 = phi(x1, x2) 选择其一。选择取决于实际经过哪个前驱;phi 不会求平均,也不会执行两条分支。如果分析证明 flag 为真,结果可以成为常量四;否则,两条可能路径仍然需要考虑。
LLVM IR 表示明确的控制流、有类型的值与内存操作。源语言的循环变成基本块和分支;失去源语法,不等于失去所有可分析的循环结构。类似寄存器的值采用 SSA,也不表示内存自动采用 SSA。别名分析仍需判断不同指针是否可能指向重叠存储。应从 LLVM 语言参考查阅指令的实际语义,而不是从源语言记法猜测。
较高层 IR 可以保留数组形状、张量操作或结构化循环,供需要这些信息的变换使用;较低层 IR 则显式表达地址运算与面向机器的操作。MLIR 的方言设计支持表示这些不同问题。选择 IR,就是选择保留哪些信息,以及让哪些变换更方便。
优化作为保持语义的转换
优化必须保持语言与编译选项要求的可观察行为,包括输出、异常、易变访问,以及允许的并发交互。对于非确定性规范,正确性不必意味着重现原程序每一种可能执行,但必须处于允许行为之内,并保持所要求的进展保证。
常量传播可以替换已知值,死代码消除可以删除没有必需效果的工作,内联则把函数体暴露给调用上下文分析。它们都不是单纯的文本替换规则。函数如果写文件,即使返回值未使用,也不能据此删除调用。把表达式移出循环,可能在原本不执行它的路径上引入异常。指针可能互为别名,因此一次存储可以使看似未变的读取失效。
算术规则也很重要。整数溢出可以回绕、触发异常或属于未定义行为,取决于语言与操作。浮点运算重新分组可以改变舍入。对数学整数有效的变换,不自动适用于每种机器数值模型。允许放宽算术要求的优化选项,会改变编译器必须保持的行为。
验证与测试提供不同证据。CompCert 在其形式化假设下,为经过验证的编译流水线证明语义保持;这不证明所有外围工具或源程序正确。生产编译器还使用回归测试、差分测试与针对具体变换的检查。调查疑似错误编译时,首先应确认源程序具有已定义的行为。
基于剖析的优化使用实际观察到的工作负载,指导内联、布局等选择。剖析可以指导收益判断,却不能单独使原本不合法的变换变得合法。应在代表性输入上测量所得程序,并在相关时纳入启动与内存使用;不存在通用的性能提升百分比。
Runtime Systems:垃圾回收与并发
追踪式垃圾回收从栈、全局变量等根出发,沿引用访问对象,并按算法回收被判定为不可达的对象。可达不等于有用:一个已过时对象如果仍被全局缓存引用,就可能长期占用内存。因此,垃圾回收避免了部分生命周期错误,但仍允许对象滞留问题。
分代收集器利用某些工作负载中许多对象分配后很快不可达的现象,需要写屏障等机制跟踪相关的跨代引用。增量回收把工作拆成小块,并发回收让回收工作与应用执行重叠,并行回收使用多个回收线程。这些是不同性质。需要一起评估暂停时间、总 CPU 工作量、额外内存与分配速率。
引用计数在拥有型引用数量降至零时回收对象,但析构可能触发很长的连锁释放,循环引用则可能使计数始终不为零。它并非自动具有有界延迟的方案。Rust 的所有权规则提供内存安全保证,但允许引用计数循环与泄漏,Rust Book 的例子展示了这一点。弱引用可以在符合预期生命周期模型时打破所有权循环。
运行时调度决定逻辑任务怎样使用操作系统线程。Go 的 goroutine 与执行 Rust future 的执行器,对调度和挂起作出不同选择。异步语法标记与大量任务都不会创造额外 CPU 容量。阻塞操作可能占据一个执行器线程,延迟无关任务,除非运行时或应用提供合适的转移执行方式。
学习这一部分会改变什么
编译器与运行时会改变实践者理解和诊断源代码与执行之间鸿沟的方式。
阅读中间代码与汇编有助于检验性能解释。编译一个小函数,改变一个条件,再检查循环是否向量化、调用是否内联。输出显示实际生成了什么,优化说明与性能剖析帮助解释原因及影响。它不会揭示所有运行时条件,也不能仅凭指令数预测性能。
运行时知识帮助检验内存增长与延迟的原因。保留对象图可以解释收集器为什么不能回收过时对象,分配与暂停测量则可以区分频繁创建短命对象和无关等待。改变运行时设置之前,应先识别它控制的行为,再测量结果。
相同思想也帮助设计小型配置或查询语言。增加优化之前,应定义有效语法、名称查找、类型、求值与错误行为。短小的语言仍需要精确含义,所需运行时设施则由这种含义决定。
资源
书籍与文本
Crafting Interpreters 构造树遍历解释器与字节码虚拟机。Engineering a Compiler 展开分析、优化与代码生成,Appel 的 Modern Compiler Implementation 围绕编译器项目组织内容。龙书 除前端外,也讲述相当多的优化与代码生成内容。The Garbage Collection Handbook 研究内存管理算法,Muchnick 的 Advanced Compiler Design and Implementation 提供详细优化讨论。应选择互补章节,而不是把整本书视为可互换对象统一排名。
| 书籍 | 作用 | 类型 |
|---|---|---|
| Nystrom, Crafting Interpreters(免费) | 易读入口,包含两个工作实现 | 实践 |
| Cooper & Torczon, Engineering a Compiler(第 3 版) | 编译器分析、优化与代码生成 | 入门 |
| Appel, Modern Compiler Implementation(ML/Java/C) | 完整项目式替代主教材 | 实践 |
| Aho, Lam, Sethi & Ullman, Compilers(Dragon Book) | 语法、语义、优化与代码生成 | 参考 |
| Jones, Hosking & Moss, The Garbage Collection Handbook(第 2 版) | GC 算法参考 | 参考 |
| Muchnick, Advanced Compiler Design and Implementation | 深入优化技术 | 深入 |
课程与讲座
Stanford CS 143 提供编译器构造路径。Cornell CS 6120 使用 Bril 与 LLVM 研究 IR、分析和优化,其练习并非全部是 LLVM 优化阶段。课程内容展示了学习顺序。Nand to Tetris 补充一套小型完整软硬件系统,有助于理解编译器在运行系统中的位置。
| 课程 | 平台 | 类型 |
|---|---|---|
| Stanford CS 143 Compilers(免费) | Stanford / course site | 入门 |
| Cornell CS 6120 Advanced Compilers(免费) | cs.cornell.edu/courses/cs6120 | 深入 |
实践、工具与当前资料
用 Compiler Explorer 比较源码与生成输出,用 LLVM Kaleidoscope 构造小型前端,并选择与 LLVM 版本匹配的文档。阅读 HotSpot、Go 或 V8 时,可以跟踪一种机制,例如分配路径、调度决策或带检查的优化,而不是一次理解整个运行时。再把源码行为与可执行例子的遥测比较。
| 资源 | 平台 | 类型 |
|---|---|---|
| Compiler Explorer(免费) | godbolt.org | 实践 |
| LLVM Kaleidoscope tutorial(免费) | llvm.org/docs/tutorial | 实践 |
| OpenJDK HotSpot source | github.com/openjdk/jdk | 参考 |
| V8 source and blog | github.com/v8/v8, v8.dev/blog | 参考 |
| Go runtime source | github.com/golang/go/tree/master/src/runtime | 参考 |
陷阱
| 误区 | 为什么会误导 | 更好的做法 |
|---|---|---|
| 把一本教材当作整个领域 | 解析、语义、优化、代码生成与运行时需要不同视角。 | 选择主要教材,再补充项目所需主题;龙书也包含相当多的优化内容。 |
| 把解析器等同于编译器 | 识别语法不确定绑定、类型、求值或目标代码。 | 跟踪小程序经过语义分析、IR 与执行。 |
| 认为运行时无关紧要 | 分配、对象滞留、调度与 JIT 工作可以影响延迟和吞吐量。 | 检查运行时遥测,把它连接到分配与执行模式。 |
| 认为教学编译器覆盖生产要求 | 调试、诊断、ABI 兼容、恢复与大型代码库增加了要求。 | 按明确语义规则扩展一个小功能,再与生产实现比较。 |
| 认为 JIT 或 AOT 总是更好 | 启动、编译成本、剖析与稳定运行分别影响选择。 | 测量整个工作负载,包括预热、内存与所需可预测性。 |
| 尚未利用信息就降低抽象层次 | 形状、循环与领域操作可以支持在低层代码中不易实现的变换。 | 比较每种 IR 保留什么,以及为何在该处安排降级转换。 |