第一章: 理解 LLVM

本文讨论了JavaScript的性能问题,特别是由于引擎差异和动态类型导致的不可预测性,以及大文件加载带来的包大小问题。介绍了WebAssembly作为解决之道,强调了LLVM在编译优化中的作用,以及如何通过LLVM和Emscripten将C/C++编译为WebAssembly以提升性能。
摘要由CSDN通过智能技术生成

JavaScript 是最受欢迎的编程语言之一。然而,JavaScript 有两个主要缺点:

  • 性能不可预测

    JavaScript 在 JavaScript 引擎提供的环境和运行时中执行。有各种各样的 JavaScript 引擎(V8、WebKit 和 Gecko)。它们都是不同构建的,并以不同的方式运行相同的 JavaScript 代码。再加上,JavaScript 是动态类型的。这意味着 JavaScript 引擎在执行 JavaScript 代码时需要猜测类型。这些因素导致了 JavaScript 执行的性能不可预测。对一种类型的 JavaScript 引擎的优化可能会在其他类型的 JavaScript 引擎上产生不良的副作用。这导致了性能的不可预测性。

  • 包大小

    JavaScript 引擎在下载整个 JavaScript 文件之前都会等待,然后才开始解析和执行。JavaScript 文件越大,等待时间就越长。这会降低应用程序的性能。像 webpack 这样的打包工具有助于最小化包大小。但当你的应用程序增长时,包大小呈指数级增长。

有没有一种工具既提供原生性能,又有更小的大小?是的,WebAssembly。

WebAssembly 是 web 和节点开发的未来。WebAssembly 是静态类型的且预编译的,因此它比 JavaScript 提供更好的性能。二进制的预编译提供了生成微小二进制包的选项。WebAssembly 允许像 Rust、C 和 C++ 这样的语言被编译成在 JavaScript 引擎中与 JavaScript 一起运行的二进制代码。所有 WebAssembly 编译器都在底层使用 LLVM 将原生代码转换成 WebAssembly 二进制代码。因此,了解 LLVM 是什么以及它如何工作是很重要的。

在本章中,我们将学习编译器的各种组件是什么以及它们是如何工作的。然后,我们将探索 LLVM 是什么以及它如何帮助编译语言。最后,我们将看到 LLVM 编译器是如何编译原生代码的。我们将在本章中涵盖以下主题:

  • 理解编译器
  • 探索 LLVM
  • LLVM 实战

技术要求

我们将使用 Clang,这是一个将 C/C++ 代码编译成原生代码的编译器。

对于 Linux 和 Mac 用户,Clang 应该是开箱即用的。

对于 Windows 用户,可以从以下链接安装 Clang:(https://llvm.org/docs/GettingStarted.html?highlight=installing clang windows#getting-the-source-code-and-building-llvm) 来安装 Clang。

理解编译器

编程语言大致分为编译型和解释型语言。

在编译型世界中,代码首先被编译成目标机器代码。这个将代码转换成二进制的过程称为 编译。将代码转换成目标机器代码的软件程序称为 编译器。在编译过程中,编译器对编写的代码进行一系列检查、传递和验证,并生成高效且优化的二进制代码。一些编译型语言的例子包括 C、C++ 和 Rust。

在解释型世界中,代码在单个传递中被读取和执行。由于编译发生在运行时,生成的机器代码不如其编译型对应物优化。解释型语言比编译型语言明显慢,但它们提供动态类型和更小的程序大小。

在本书中,我们将只关注编译型语言。

编译型语言

编译器是一种翻译器,它将源代码翻译成机器代码(或以更抽象的方式,将代码从一种编程语言转换成另一种)。编译器之所以复杂,是因为它必须理解源代码所使用的语言(其语法、语义和上下文);它还必须理解目标机器代码(其语法、语义和上下文),并且必须创建一个将源代码映射到目标机器代码的表示。

编译器具有以下组件:

  • 前端 - 前端负责处理源语言。
  • 优化器 - 优化器负责优化代码。
  • 后端 - 后端负责处理目标语言。

编译器的前端首先分析源代码,检查语法错误,并将代码转换成一个中间表示(IR)。IR 是一种抽象表示,它独立于源代码和目标平台。优化器在这个中间表示上执行各种优化,以提高代码的性能和效率。后端则将优化后的 IR 转换成目标平台的机器代码。

在编译过程中,编译器的不同阶段将源代码转换成不同的形式,以便进行分析和优化。编译器通过分析和优化源代码,生成高效且优化的目标代码。由于这个过程涉及到复杂的分析和优化,所以编译型语言通常比解释型语言运行得更快。

 Figure 1.1 – Components of a compiler

第1.1图 - 编译器的组件

前端

前端专注于处理源语言。它在接收代码时进行解析。然后,代码会被检查是否有任何语法或语法问题。之后,代码被转换(映射)为中间表示IR)。将 IR 视为表示编译器处理的代码的格式。IR 是编译器对你代码的版本。

优化器

编译器中的第二个组件是优化器。这是可选的,但如其名称所示,优化器分析 IR 并将其转换为更高效的形式。少数编译器具有多个 IR。编译器在每次遍历 IR 时都会高效地优化代码。优化器是一个 IR 到 IR 的转换器。优化器进行分析、运行传递并重写 IR。这里的优化包括去除冗余计算、消除死代码(无法到达的代码)以及将在后续章节中探索的各种其他优化选项。值得注意的是,优化器不必特定于语言。由于它们作用于 IR,它们可以构建为通用组件,并与多种语言一起重用。

后端

后端专注于生成目标语言。后端接收生成的(优化的)IR,并将其转换为另一种语言(例如机器代码)。也可以链接多个后端以将代码转换为其他语言。后端负责从 IR 生成目标机器代码。这种机器代码是在裸机上运行的实际代码。为了生成高效的机器代码,后端应该理解代码执行的架构。

机器代码是一组指令,指导机器在寄存器中存储一些值并对其进行计算。例如,生成的机器代码负责在 32 位架构中有效地将 64 位数字存储在空闲寄存器中(等等)。后端应理解目标环境,以有效地创建一组指令,并正确选择和安排指令,以提高应用程序执行的性能。

编译器效率

执行速度越快,性能越好。

编译器的效率取决于它如何选择指令,在给定架构中如何分配寄存器以及如何安排指令执行。指令集是处理器支持的一组操作,这种总体设计称为指令集体系结构ISA)。ISA 是计算机的抽象模型,通常被称为计算机架构。不同的处理器以不同的实现方式转换 ISA。不同的实现在性能上可能有所不同。ISA 是硬件和软件之间的接口。

如果你正在实现一种新的编程语言,并且希望这种语言在不同的架构(或更抽象地说,不同的处理器)上运行,那么你应该为这些架构/目标构建后端。但是,为每个架构构建这些后端是困难的,开发一种语言将需要时间、成本和努力。

如果我们创建一个通用的 IR,并构建一个将此 IR 转换为在各种架构上高效运行的机器代码的编译器呢?让我们称这种编译器为低级虚拟机。现在,你的编译器链中的前端的角色仅仅是将源代码转换为与低级虚拟机(例如 LLVM)兼容的 IR。现在,低级虚拟机的一般目的是作为一个通用的可重用组件,将 IR 映射到各种目标的本机代码。但低级虚虚拟机只理解通用 IR。这种 IR 被称为 LLVM IR,而编译器被称为 LLVM

探索 LLVM

LLVM 是 LLVM 项目的一部分。LLVM 项目托管编译器和工具链技术。LLVM 核心 是 LLVM 项目的一部分。LLVM 核心负责提供源代码和目标代码无关的优化,并为许多 CPU 架构生成代码。这使得语言开发人员只需创建一个生成与 LLVM 兼容的 IR 或 LLVM IR 的前端即可。

您可能不知道的是,LLVM 并不是一个首字母缩写词。当该项目最初作为一个研究项目启动时,它意味着低级虚拟机。但后来,决定将这个名称用作非缩写词。

LLVM 的主要优势如下:

  • LLVM 使用类似于 C 的简单低级语言。
  • LLVM 是强类型的。
  • LLVM 有严格定义的语义。
  • LLVM 有准确和精确的垃圾回收。
  • LLVM 提供各种优化,您可以根据需求选择。它有激进的标量的过程间的简单循环的基于配置文件的优化。
  • LLVM 提供多种编译模型,包括链接时安装时运行时离线
  • LLVM 为多种目标架构生成机器代码。
  • LLVM 提供 DWARF 调试信息。

请注意,LLVM 并非一个单一的整体项目。它是一个包含子项目和其他项目的集合。这些项目被各种语言(如 Ruby、Python、Haskell、Rust 和 D)用于编译。

了解了编译器和 LLVM 的知识后,我们将看到它是如何被使用的。

LLVM 实战

在本节中,我们将使用 LLVM 的 Clang 编译器将原生代码编译成 LLVM IR。这将更好地展示 LLVM 的工作方式,并有助于理解未来章节中编译器如何使用 LLVM。

首先,我们创建一个名为 sum.c 的 C 文件,并输入以下内容:

$ touch sum.c
// sum.c
unsigned sum(unsigned a, unsigned b) {
   return a + b;
} 

sum.c 文件包含一个简单的 sum 函数,接收两个无符号整数并返回它们的和。LLVM 提供了 Clang LLVM 编译器来编译 C 源代码。为了生成 LLVM IR,请运行以下命令:

$ clang -S -O3 -emit-llvm sum.c

我们为 Clang 编译器提供了 -S-O3-emit-llvm 选项:

  • -S 选项指定编译器仅执行预处理和编译步骤。
  • -O3 选项指定编译器生成优化良好的二进制文件。
  • -emit-llvm 选项指定编译器在生成机器代码的同时发出 LLVM IR。

前面的代码将打印出以下 LLVM IR:

define i32 @sum(i32, i32) local_unnamed_addr #0 {
  %3 = add i32 %1, %0
  ret i32 %3
} 

LLVM IR 的语法在结构上更接近于 C。define 关键字定义了函数的开始。紧随其后的是函数的返回类型 i32。接下来,我们有函数的名称 @sum

重要说明

注意那里的 @ 符号吗?LLVM 使用 @ 来标识全局变量和函数。它使用 % 来标识局部变量。

在函数名称之后,我们声明输入参数的类型(在此案例中为 i32)。local_unnamed_addr 属性表明在模块中该地址不被认为是重要的。LLVM IR 中的变量是不可变的。也就是说,一旦你定义了它们,就不能改变它们。所以在 block 内部,我们创建了一个新的局部值 %3,并将其值设置为 addadd 是一个操作码,它接收参数的 type 以及两个参数 %0%1%0%1 表示第一和第二个局部变量。最后,我们使用 ret 关键字和 type 返回 %3

这个 IR 是可转换的;也就是说,IR 可以从文本表示转换成内存中的表示,然后转换成在裸机上运行的实际位码。同样,从位码,你可以将它们转换回文本表示。

想象一下,你正在编写一种新语言。该语言的成功取决于它在各种架构上的性能表现。为各种架构(如 x86、ARM 等)生成优化的字节码需要很长时间,并且并不容易。LLVM 提供了一种简单的方式来实现它。不是针对不同的架构,而是创建一个编译器前端,将源代码转换成与 LLVM 兼容的 IR。然后,LLVM 将 IR 转换成在任何架构上运行的高效且优化的字节码。

注意

LLVM 是一个伞形项目。它有很多组件,你可以写一系列书籍来介绍它们。全面覆盖 LLVM 以及如何安装和运行它们超出了本书的范围。如果你对 LLVM 的各种组件、它们的工作方式以及如何使用它们感兴趣,可以查看网站:https://llvm.org

总结

在本章中,我们了解了编译语言的工作原理以及 LLVM 如何帮助编译它们。我们使用 LLVM 编译了一个示例程序,以理解它的工作方式。在下一章中,我们将探讨 Emscripten,这是一个将 C/C++ 转换成 WebAssembly 模块的工具。Emscripten 使用 LLVM 后端进行编译。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值