[关闭]
@Rays 2018-02-07T11:25:59.000000Z 字数 4277 阅读 2221

LLVM:Swift、Rust、Clang等语言的强大后盾

语言开发


摘要: 在软件开发领域,我们看到一些新的开发语言和改进如雨后春笋般涌现。它们为开发人员在开发速度、安全性、便利性、可移植性和功能上提供了多种选择。这可部分归因于我们具备了构建语言尤其是编译器的新工具,其中首屈一指的就是LLVM。LLVM不仅简化了新语言的创建工作,而且提升了现有语言的发展。本文介绍了LLVM的功能和使用机制,并未来发展做了展望。

作者: Serdar Yegulalp

正文:

在软件开发领域,一些新的开发语言和对已有语言的改进如雨后春笋般涌现。我们看到了Mozilla RustApple Swift, Jetbrains Kotlin及更多语言的推陈出新。这些语言为开发人员在开发速度、安全性、便利性、可移植性和功能上提供了多种选择。

为什么会是当下?其中一个重要原因,就是我们具备了构建语言尤其是编译器的新工具。其中首屈一指的就是LLVM(Low-Level Virtual Machine)。LLVM是一个开源项目,最初是由Swift语言创始人Chris Lattner以伊利诺伊大学的一个研究项目为基础发展而来。

LLVM不仅简化了新语言的创建工作,而且提升了现有语言的发展。它提供了一种工具,自动化了创建语言任务中许多最吃力的部分,包括创建编译器、将输出代码移植到多个平台和架构上,以及编写代码实现异常处理这样的常见语言隐喻(metaphor)。LLVM是自由许可的,这意味着它可作为软件组件自由重用,也可以作为服务自由部署。

如果列出一份使用了LLVM的语言清单,我们能从中看到许多耳熟能详的名字。例如,Apple的Swift语言使用LLVM作为编译器框架,Rust使用LLVM作为工具链的核心组件。此外,很多编译器也提供了LLVM版本。例如,Clang这个C/C++编译器本身就是一个以LLVM为准绳的项目。还有Kotlin,它名义上是一种JVM语言,使用称为Kotlin Native的语言开发,该语言也使用了LLVM编译机器原生代码。

LLVM简介

LLVM本质上是一个使用编程方式创建机器原生代码的软件库。开发人员调用其API,生成一种使用“中间表示”(IR,Intermediate Representation)格式的指令。进而,LLVM将IR编译为独立软件库,或者使用另一种语言的上下文(例如,使用该语言的编译器)对代码执行JIT(即时,just-in-time)编译。

LLVM API提供了一些原语,用于表示开发编程语言中常见结构和模式。例如,几乎所有的语言都具有函数和全局变量的概念。LLVM也将函数和全局变量作为IR的标准元素。这样,开发人员可以直接使用LLVM的实现,并聚焦于自身语言中的独到之处,不再需要花费时间和精力去重造这些特定的轮子。


图1 一个LLVM IR的例子。图右侧显示了一个使用C编写的简单程序,左侧显示了使用Clang编译器转换得到的LLVM IR代码

LLVM:为可移植性而设计

我们通常对C语言的认识,可套用到对LLVM的认识上。我们时常将C语言看成是一种可移植的高层汇编语言,因为C中提供了一些直接映射到系统硬件的结构,并已移植到近乎所有现有的系统架构上。但是作为一种可移植的汇编语言并非C语言的设计目标,这只是由该语言的工作机制所提供的一个副产品。

与此不同,LLVM IR的设计从一开始,就是要成为一种可移植的汇编语言。IR实现可移植性的方式之一,就是提供了独立于任何特定机器架构的原语。例如,整数类型可使用任何所需的位数,甚至大到128位整数,不会受限于机器的最大位宽度。开发人员也无需为匹配某种特定处理器的指令集,考虑如何对输出做精雕细琢。LLVM解决了所有这一切。

如果读者想实地查看LLVM IR的运行情况,推荐访问ELLCC项目网站,并可动手在浏览器中尝试一个将C代码转换为LLVM IR的现场演示

在编程语言中使用LLVM

LLVM通常作为语言的AOT(预先编译,ahead-of-time)编译器使用。此外,LLVM还支持其它一些功能。

使用LLVM的JIT编译器

在一些情况下,需要代码在运行时直接生成,而不是做预先编译。例如,Julia语言就对代码做JIT编译,因为它看重的是运行速度,并可通过REPL(读取-求值-输出循环,read-eval-print loop)或交互式提示符与用户交互。.NET的开源实现Mono也提供了选项,支持通过LLVM后端方式编译生成原生代码

Python的高性能科学计算库Numba将设定的Python函数JIT编译为机器代码,也可以对使用了Numba的代码做AOT编译。但是作为一种解释性语言,Python与Julia一样也提供了快速开发。使用JIT编译代码,是对Python交互工作流的一种很好的补充,要优于使用AOT编译。

还有一些非正统的方法,也尝试使用LLVM作为JIT。例如,有方法尝试编译PostgreSQL查询,并实现了性能翻五番。


图2 Numba使用LLVM对科学计算代码做JIT编译,加速了代码的执行。例如,经JIT加速的sum2d函数,要比常规Python代码的执行速度快139倍

使用LLVM做自动代码优化

LLVM不仅将IR编译为原生机器代码,开发人员也可以通过编程方式,指导LLVM使用链接过程对代码做高度精细的优化。这种优化卓有成效,其中涉及内联函数、去除死代码(包括未使用的类型定义和函数参数)和循环展开(loop unrolling)等。

同样,LLVM的强大之处在于无需开发人员自己去实现所有这些功能。LLVM包揽了所有一切,而且开发人员可在需要时关闭这些功能。例如,如果我们考虑牺牲一些性能去给出更小的二进制文件,可以让编译器前端告知LLVM禁止循环展开。

使用LLVM的领域特定语言(DSL)

通常,LLVM用于生成通用语言编译器。但是,LLVM也可用于生成一些高度垂直或排他性DSL。我们甚至可以说,这正是LLVM大显身手之处。因为在使用LLVM创建一种DSL时,无需亲历亲为创建语言中的大量苦差事,并可给出良好的表现。

例如,Emscripten项目使用LLVM IR,并将IR代码转化为JavaScript。这将在理论上支持所有具有LLVM后端的语言导出可运行在浏览器中的代码。尽管Emscripten的长期计划是使用基于LLVM的后端生成WebAssembly,但是该项目很好地展示了LLVM的灵活性。

另一种使用LLVM的方式,是将领域特定的扩展添加到现有的语言中。例如,Nvidia使用LLVM创建了Nvidia CUDA编译器,实现在语言中添加对CUDA的原生支持,并作为所生成的原生代码的一部分做编译,而不是通过随之一起交付的软件库做调用。

在各种语言中使用LLVM

LLVM的通常使用方式,是编码在开发人员顺手的开发语言中。当然,该语言应支持LLVM软件库。

其中,广为采用的C和C++。不少LLVM开发人员二者必取其一,理由是:

当然,选择并不局限于这两种语言。不少语言支持原生地调用C软件库。因此在理论上讲,可以使用任何一种此类语言做LLVM开发。当然,如果语言本身就提供包装了LLVM API的软件库,这样最好。幸运的是,很多语言和运行时都具有这样的软件库,其中包括C#/.NET/MonoRustHaskellOCAMLNode.jsGoPython

需要给出警告的是,部分语言对LLVM的绑定尚不完备。以Python为例。尽管Python提供了多种选择,但每种选择的完备性和实用性各有千秋:

如果有兴趣了解如何使用LLVM软件库构建一种语言,可以阅读由LLVM创始人撰写的教程。该教程使用C++和OCAML,一步步引导读者去创建一个名为“Kaleidoscope”的简单语言。进而移植到其它语言中:

该教程还有其它一些国家语言的翻译版本,例如使用原始C++Python的中文教程。

LLVM尚未实现的

我们上面介绍了LLVM提供的很多功能,下面简述一下它目前尚未实现的。

例如,LLVM并不对语法做解析。因为有大量工具可用于完成这个工作,例如lex/yaccflex/bisonANTLR。解析必定会从编译中脱离出来,因此毫不奇怪LLVM并未试图去实现该功能。

LLVM也不直接解决大部分针对特定语言的软件文化。例如,如何安装编译器的二进制文件,如何在安装中管理软件包,如何升级工具链等,这都需要开发人员自己去做。

最后也是最重要的一点是,LLVM仍然尚未对部分通用语言成分给出原语。许多语言都具有某种垃圾回收的内存管理方式,或者是作为管理内存的主要方式,或者是作为对RAII(C ++和Rust使用)等策略的附属方式。LLVM并没有提供垃圾收集机制,而是提供了一些实现垃圾回收的工具,支持将代码标记为一些可简化垃圾收集器编写的元数据。

但是,并不排除LLVM可能最终会添加实现垃圾回收的本地机制。LLVM正在以每六个月发布一个主要版本的速度快速发展。鉴于当前许多语言的开发过程是以LLVM为中心的,所以LLVM的开发速度只可能会进一步提升。

查看英文原文: What is LLVM? The power behind Swift, Rust, Clang, and more

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注