什么是LLVM?Swift, Rust, Clang等语言背后的支持
要了解用于以编程方式生成机器原生代码的编译器框架是如何让新语言的推出以及对现有的语言进行增强比以往更加容易了。
新的语言,还有对现有语言的提升,在整个编程环境中正大行其道。Mozilla 的 Rust、Apple 的 Swift、Jetbrains 的 Kotlin,以及许多其它的语言都给开发者在速度、安全性、便利性、可移植性还有能力这些方面提供了新的选择。
为什么现在正当时呢?一个大因素就是那些用来构建语言的新工具,特别是编译器。它们中首当其冲就是 LLVM (底层虚拟机 Low-Level Virtual Machine),这是一个开源项目,最开始作为伊利诺伊州大学的一个研究项目由 Swift 语言的创始人 Chris Lattner 进行开发。
LLVM 使创建新语言变得更加容易,同时也可以增强现有语言的开发。它提供了一些工具,用于自动执行语言创建任务中最不讨人喜欢的部分:创建一个编译器,将输出的代码移植到多个平台和架构,编写代码来处理常见的语言隐喻,比如异常。它的自由授权意味着它可以自由地作为软件组件重用或作为服务部署。
使用 LLVM 的语言名册中有许多熟悉的名字。苹果的 Swift 语言使用 LLVM 作为它的编译器框架,而 Rust 则将 LLVM 作为其工具链的核心组件。而且,许多编译器都有一个 LLVM 版本,如 Clang、C/C++ 编译器(这个名称叫做“C-lang”),它本身就是一个与 LLVM 紧密相连的项目。而 Kotlin,名义上是一种 JVM 语言,正在开发一种名为 Kotlin Native 的语言版本,它使用 LLVM 来编译成机器原生代码。
LLVM 定义
在它的核心,LLVM 是一个以编程方式创建机器原生代码的库。开发人员使用该 API 以一种称为中间代理或 IR 的格式生成指令。然后 LLVM 可以将 IR 编译成一个独立的二进制文件,或者在另一个程序(如语言解释器)的上下文中执行 JIT (just-in-time) 编译。
LLVM 的 API 为开发在编程语言中发现的许多常见结构和模式提供了原始的方式。例如,几乎每种语言都有函数和全局变量的概念。LLVM 将函数和全局变量作为其 IR 中的标准元素,因此,你只需在意 LLVM 的实现,并关注需要注意的语言部分,而不是花费时间和精力重新创建这些特定的轮子。
这是一个 LLVM 中间代理(IR)的例子。右边是一个简单的 C 程序;左边是由 Clang 编译器翻译成 LLVM IR 的代码。
LLVM:专为可移植性而生
关于 LLVM 的一个说法是它像常提到的 C 编程语言:C 语言有时候被认为是一种便携式、高级的汇编语言,因为它可以紧密地映射到系统硬件的结构,而且它已经被移植到几乎所有的系统架构。但是,C 语言只是作为一种可移植的汇编语言,是其工作方式的另一种效果;这并不是它的设计目标之一。
相比之下,LLVM 的 IR 是从一开始就设计为可移植的组件。它实现这种可移植性的一种方法是提供独立于任何特定机器架构的原语。例如,整数类型不局限于底层硬件的最大位宽度(例如 32 或 64 位),您可以根据需要使用尽可能多的比特字节来创建基本的整数类型,比如 128 位整数。您也不必担心手工输出来匹配特定处理器的指令集;LLVM 也会为你处理这个问题。
如果你希望看到 LLVM IR 的现场示例,请访问 ELLCC 项目网站,并尝试在浏览器中将 C 代码转换为 LLVM IR 的现场演示 Demo。
编程语言中如何使用 LLVM
LLVM 最常见的用例是作为一种语言的预先(AOT ahead-of-time)编译器。但 LLVM 也可以用于即时编译。
用 LLVM 进行即时编译
有些情况下需要在运行时动态生成代码,而不是预先编译。例如,Julia 语言就是使用 JIT 编译代码,因为它需要快速运行,并通过 REPL(read-eval-print loop)或交互式提示与用户交互。.Net 和 Mono 可以选择通过 LLVM 后端方式编译为原生代码。
Numba 是一个 Python 的数学加速包,JIT 将所选择的 Python 函数编译成机器码。它也可以预先编译使用 Numba 装饰器装饰的代码,但是(比如 Julia)Python 作为一种快速发展的解释性语言,使用 JIT 编译来产生这样的代码更好地补充了 Python 的交互式工作流,比 Python 的预先编译方式更好。
其他人正在尝试以非正统方式使用 LLVM 作为 JIT 编译方式,例如编译 PostgreSQL 查询,据说性能提高了五倍。
Numba 使用 LLVM 进行即时编译数字代码并加速其执行。JIT 加速过的 sum2d 函数的执行速度比常规 Python 代码快 139 倍。
使用 LLVM 进行自动代码优化
LLVM 不仅将 IR 编译为原生机器码。你也可以直接以编程的方式在整个链接过程中高度精细地优化代码。优化方式是相当积极主动的,能够实现包括内联函数在内,消除死代码(包括未使用的类型声明和函数参数)和展开循环这些事情。
这里再一次强调,LLVM 的力量让你不必自己实现所有这一切。LLVM 可以为您处理它们,您也可以根据需要直接禁用。例如,如果你想要一些更小的二进制代码,那么你可以让你的编译器告诉 LLVM 禁用循环展开。
使用 LLVM 的领域特定语言
LLVM 已被用于生成多种通用语言的编译器,但它也可用于生成高度垂直或排他性问题域的语言。从某种意义上说,这就是 LLVM 最闪光的地方,因为它在创造这样一类语言方面消除了诸多苦差事,并使其表现良好。
例如,Emscripten 项目采用 LLVM IR 代码并将其转换为 JavaScript,理论上支持使用 LLVM 作为后端的任何语言导出可在浏览器中运行的代码。长期规划是支持基于 LLVM 的后端并能够生成 WebAssembly 代码,Emscripten 是 LLVM 灵活性的一个很好的例子。
LLVM 可以被使用的另一种方法是将特定领域的扩展添加到现有语言。Nvidia 使用 LLVM 创建了 Nvidia CUDA 编译器,该编译器允许语言为 CUDA 添加原生支持,它是作为你生成的原生代码的一部分编译的,而不是通过附带的库进行调用的。
在不同语言中使用 LLVM
使用 LLVM 的典型方式是通过你所熟悉的语言来编写代码(当然也要有支持 LLVM 的库)。
两种常见的可选语言是 C 和 C++。许多 LLVM 开发者会因为以下的原因而默认选择其中的一个:
- LLVM 本身是用 C++ 编写的
- LLVM 的 API 以 C 和 C++ 版本提供
- 大量的语言开发往往会以 C/C++ 作为一个基础
不过,这两种语言并不是唯一的选择。许多语言都可以原生调用 C 语言库,所以理论上可以用任何这样的语言进行 LLVM 开发。但需要有一个实际的语言库可以很好地封装 LLVM API。幸运的是,许多语言和语言运行时都有这样的库,包括 C#/.Net/Mono, Rust, Haskell, OCAML, Node.js, Go, 和 Python。
需要注意的是,一些与 LLVM 的语言绑定可能不完整。以 Python 为例,有很多种绑定选择,但每个选项的完整性和实用性各不相同:
LLVM 项目维护着自己的一套到 LLVM 的 C API 的绑定,但是目前他们没有继续维护。
llvmpy 在 2015 年后就没有进行维护了 —— 这对于任何软件项目都是不利的,在使用 LLVM 时更是如此,因为每个版本的 LLVM 都有一些变化。
由创建 Numba 的团队开发的 llvmlite 已经成为当前在 Python 中的 LLVM 的竞争者。它只实现了 LLVM 功能的一个子集,正如 Numba 项目的需求所规定的那样。但是这个子集满足了绝大多数 LLVM 用户所需。
llvmcpy 旨在为 C 库带来最新的 Python 绑定,它以自动化的方式保持更新,并使用 Python 的习惯用法来访问它们。llvmcpy 还处于早期阶段,但是已经可以用 LLVM API 做一些基本的工作。
如果你对如何使用 LLVM 库构建语言感兴趣,不妨看看 LLVM 的创建者撰写的使用 C++ 或 OCAML 语言的教程,它将一步步指导你创建一种简单的名为 Kaleidoscope 的语言。它还被移植到其他语言之上:
- Haskell:参考原始教程的直接移植。
- Python:在此网站的教程和原始版本非常相近,而另一个版本则是用交互式命令行进行更为雄心勃勃的重写。这两种版本都使用 llvmlite 作为到 LLVM 的绑定。
- Rust 和 Swift:不可避免地,我们不得不将该教程移植到这两种语言之上,它们都是由 LLVM 自身帮助使其诞生的。
最后,这个教程也有其他国家语言版本的。这里有中文版,分别是使用原始的 C++ 和 Python 版本。
LLVM 尚未实现的功能
了解 LLVM 可以实现的功能的同时,有必要知道 LLVM 目前尚未实现的功能。
例如,LLVM 不解析语言的语法。因为目前已经有许多工具实现这个功能,比如 lex/yacc, flex/bison,以及ANTLR。解析语法就意味着必须从编译中解耦出来,难怪 LLVM 并没有涉及这个领域。
LLVM 也不会直接干涉到开发语言的软件文化,比如安装编译器的二进制文件、如何在安装中管理软件包、升级工具链 —— 这些都需要开发者自己去实现。