编译器目标平台简析
作为业余编译器开发者,我经常为一个决定而纠结:选择哪个编译器后端目标。不同于80年代需要直接面向各种机器架构,现在有许多成熟的选择。这是一份简短且不完整的调查,介绍一些流行且有趣的选项。
目录
- 机器码/汇编
- 中间表示
- 其他高级语言
- 虚拟机/字节码
- WebAssembly
- 元追踪与元编译框架
- 非传统目标平台
- 结论
机器码/汇编
编译器总是可以直接输出针对一种或多种架构的机器码或汇编代码。著名例子是 Tiny C Compiler(TCC),它以快速和小巧著称,可以即时编译运行C代码。另一个例子是 Turbo Pascal。你也可以为自己的编译器这样做,但需要弄清楚每个目标架构的指令集架构(ISA)细节,以及寄存器分配等概念。
大多数现代编译器实际上并不直接生成机器码或汇编。它们先将源代码降维到一种与语言无关的中间表示(IR),然后由此为各大架构(x86-64、ARM64等)生成机器码。
这个领域最突出的工具是LLVM。这是一个庞大的开源编译器库。许多语言的编译器如Rust、Swift、C/C++(通过Clang)和Julia都使用LLVM作为IR来生成机器码。
另一个选择是GNU Compiler Collection(GCC),通过其GIMPLE IR,不过似乎没有编译器直接使用它。GCC可以像LLVM一样作为库使用,通过libgccjit编译代码。它在Emacs中用于即时编译(JIT)Elisp。Cranelift是该领域的另一个新选择,不过它支持的ISA较少。
对于那些觉得LLVM或GCC太大或编译太慢的人,存在极简主义替代品。QBE是一个专注于简洁的小型后端,目标是在"10%的代码量中实现70%的性能"。它被强调快速编译的语言Hare所使用。另一个选择是libFIRM,它使用基于图的SSA表示而非线性IR。
其他高级语言
有时你乐意让其他编译器/运行时来处理繁重工作。你可以将代码转译到另一个成熟的高级语言,并利用该语言的现有编译器/运行时和工具链。
这种情况下常见目标是C语言。由于几乎所有平台都有C编译器,生成C代码使你的语言具有高度可移植性。Chicken Scheme和Vala采用这种策略。或者你也可以编译到C++,比如Jank(如果你偏好这个)。还有C–,一个被GHC和OCaml作为目标的C子集。
另一个无处不在的目标是JavaScript(JS),这是在浏览器或JS运行时(Node、Deno、Bun)中原生运行代码的两种选择之一(另一种是WebAssembly)。多种语言如TypeScript、PureScript、Reason、ClojureScript、Dart和Elm都转译到JS。有趣的是,Nim可以转译到C、C++或JS。
另一个类似JS的目标是Lua,一种轻量级且可嵌入的脚本语言,MoonScript和Fennel等语言转译到它。
更小众的方法是面向Lisp方言。例如编译到Chez Scheme,让你能利用它的宏系统、运行时和编译器。Idris 2和Racket都使用Chez Scheme作为主要后端目标。
虚拟机/字节码
这是应用型语言的常见选择。你编译到虚拟机的可移植字节码。虚拟机通常带有垃圾回收、JIT编译和安全沙箱等功能。
Java虚拟机(JVM)可能是最流行的。它是许多语言的目标,包括Java、Kotlin、Scala、Groovy和Clojure。其主要竞争对手是公共语言运行时(CLR),最初由微软开发,被C#、F#和Visual Basic.NET等语言作为目标。
另一个值得注意的虚拟机是BEAM,最初为Erlang构建。BEAM VM不是为了原始计算速度,而是为了高并发、容错性和可靠性而设计。最近,Elixir和Gleam等新语言被创建来面向它。
最后,这一类别还包括MoarVM——Parrot VM的精神继承者,为Raku(原名Perl 6)语言构建。
WebAssembly
WebAssembly(Wasm)是一个相对较新的目标。它是一种可移植的二进制指令格式,专注于安全性和效率。Wasm被所有主流浏览器支持,但不局限于此。WebAssembly系统接口(WASI)标准为在非浏览器和非JS环境中运行Wasm提供了API。现在许多语言如Rust、C/C++、Go、Kotlin、Scala、Zig和Haskell都面向Wasm。
元追踪和元编译框架是更复杂的类别。这些不是你编译器后端的目标,而是你用它们通过为你的语言指定解释器来构建自定义JIT编译器。
最著名的例子是PyPy,一个使用RPython框架创建的Python实现。另一个这样的框架是GraalVM/Truffle,来自Oracle的多语言VM和元追踪框架。其主要特性是零成本互操作:GraalJS、TruffleRuby和GraalPy的代码都可以在同一个VM上运行,并能直接相互调用。
非传统目标平台
进入非主流领域,你会发现一个充满非传统和深奥编译器目标的世界。开发者选择它们出于学术好奇、艺术表达,或测试可行编译目标的边界。
-
Brainfuck:一种只有八个命令的深奥语言,Brainfuck是图灵完备的,并已成为编译器挑战的目标。人们已经为C、Haskell和Lambda演算编写了编译器。
-
Lambda演算:Lambda演算是一种极简编程语言,仅将计算表示为函数及其应用。由于其简单性,它常被用作教育编译器的目标,以及其与计算本质的联系。Hell,一个Haskell子集,编译到简单类型Lambda演算。
-
SKI组合子:SKI组合子演算比Lambda演算更极简。SKI演算中的所有程序都可以由仅三个组合子S、K和I组成。MicroHs将Haskell子集编译到SKI演算。
-
JSFuck:你知道可以用仅六个字符
[]()!+编写所有可能的JavaScript程序吗?好了,现在你知道了。 -
Postscript:Postscript也是图灵完备编程语言。你的下一个编译器可以面向它!
-
正则表达式?乐高?元胞自动机?
专有名词解释
| 术语 | 英文 | 解释 |
|---|---|---|
| 编译器后端 | Compiler Backend | 编译器的后端阶段,负责将中间代码优化并转换为目标平台的机器码或字节码 |
| Tiny C Compiler | TCC | 极小巧快速的C语言编译器,能即时编译执行C代码 |
| Turbo Pascal | - | Borland公司开发的Pascal集成开发环境,以编译速度快闻名 |
| 指令集架构 | ISA (Instruction Set Architecture) | 定义处理器支持的所有指令及其编码方式的规范,如x86-64、ARM等 |
| 寄存器分配 | Register Allocation | 编译器优化技术,决定程序变量应存放在CPU寄存器还是内存中,影响执行效率 |
| 中间表示 | IR (Intermediate Representation) | 源代码与目标代码之间的抽象表示形式,用于简化编译器设计与优化 |
| LLVM | - | 大规模开源编译器基础设施项目,提供模块化编译器组件和工具链 |
| Clang | - | LLVM项目的C/C++/Objective-C编译器前端 |
| GNU编译器集合 | GCC | GNU项目的编译器套装,支持多种语言 |
| GIMPLE | - | GCC使用的中间表示形式 |
| libgccjit | - | GCC的即时编译库,允许将GCC作为库嵌入其他程序 |
| 即时编译 | JIT (Just-in-time) | 程序运行时动态编译代码的技术,提升执行效率 |
| Cranelift | - | 用Rust编写的新型代码生成后端,注重快速编译 |
| QBE | - | 极简主义编译器后端,追求小体积与高性能的平衡 |
| Hare | - | 注重编译速度和系统编程的现代编程语言 |
| libFIRM | - | 使用基于图的SSA形式的编译器库 |
| SSA | Static Single Assignment | 静态单赋值形式,一种常用的中间表示形式,每个变量只赋值一次 |
| 转译 | Transpile | 将一种高级语言源代码转换为另一种高级语言源代码的过程 |
| Chicken Scheme | - | 将Scheme编译到C的编译器 |
| Vala | - | 类似C#语法的编程语言,编译到C代码 |
| Jank | - | Clojure方言,可编译到C++ |
| C– | - | 面向C的简化中间语言,用于函数式语言编译 |
| GHC | Glasgow Haskell Compiler | Haskell语言的主要编译器 |
| OCaml | - | 函数式编程语言 |
| Node/Deno/Bun | - | JavaScript运行时环境 |
| TypeScript | - | 微软开发的JavaScript超集,添加静态类型 |
| PureScript | - | 强类型纯函数式语言,编译到JavaScript |
| Reason | - | Facebook开发的编程语言,可编译到JavaScript |
| ClojureScript | - | Clojure到JavaScript的编译器 |
| Dart | - | Google开发的编程语言 |
| Elm | - | 函数式前端编程语言,编译到JavaScript |
| Nim | - | 静态类型的系统编程语言,可编译到多种目标 |
| MoonScript | - | 编译到Lua的编程语言 |
| Fennel | - | Lisp方言,编译到Lua |
| Chez Scheme | - | 优化编译器,Scheme实现之一 |
| Idris 2 | - | 支持依赖类型的函数式编程语言 |
| Racket | - | Lisp方言及通用编程语言平台 |
| 字节码 | Bytecode | 虚拟机执行的中间代码形式,比机器码更抽象 |
| 虚拟机 | VM (Virtual Machine) | 在软件层面模拟的计算机,执行字节码 |
| Java虚拟机 | JVM | Java平台的核心运行时环境 |
| Kotlin/Scala/Groovy | - | 运行在JVM上的编程语言 |
| 公共语言运行时 | CLR | .NET平台的运行时环境 |
| C#/F#/VB.NET | - | .NET生态系统中的编程语言 |
| BEAM | - | Erlang虚拟机,支持高并发和容错 |
| Erlang | - | 并发函数式编程语言 |
| Elixir/Gleam | - | 运行在BEAM虚拟机上的现代编程语言 |
| MoarVM | - | Rakudo Perl 6的运行时虚拟机 |
| Raku | - | 原名Perl 6,现代通用编程语言 |
| WebAssembly | Wasm | 可移植的二进制指令格式,可在浏览器和服务器运行 |
| WebAssembly系统接口 | WASI | 在非浏览器环境中运行Wasm的系统接口标准 |
| Zig | - | 现代系统编程语言 |
| 元追踪 | Meta-tracing | 通过追踪解释器执行路径来自动生成JIT编译器的技术 |
| 元编译 | Metacompilation | 编译器生成技术,用高级语言编写解释器来自动获得编译器 |
| PyPy | - | 使用RPython实现的Python解释器,含JIT |
| RPython | - | Python的静态类型子集,用于编写虚拟机 |
| GraalVM/Truffle | - | Oracle的多语言运行时和元编译框架 |
| GraalJS/TruffleRuby | - | GraalVM上的JavaScript和Ruby实现 |
| 零成本互操作 | Zero-cost Interoperability | GraalVM中不同语言间无额外开销的互调用能力 |
| Brainfuck | - | 极简的深奥编程语言,仅8个指令 |
| 图灵完备 | Turing-complete | 计算系统能够模拟任意图灵机,理论上可计算所有可计算问题 |
| Lambda演算 | Lambda Calculus | 函数式编程的理论基础,用函数抽象表达计算 |
| Hell | - | Haskell的子集,编译到简单类型Lambda演算 |
| SKI组合子 | SKI Combinators | 仅含S、K、I三个组合子的极简计算系统 |
| MicroHs | - | 微型Haskell编译器 |
| JSFuck | - | 仅用6个字符[]()!+编写JavaScript程序的混淆技术 |
| Postscript | - | 页面描述语言,同时也是图灵完备编程语言 |