10.万花筒:结论和其他有用的LLVM花絮

  • 教程结论
  • LLVM IR的属性
  • 目标独立
  • 安全保障
  • 语言特定优化
  • 技巧和窍门
  • 实现便携式offsetof / sizeof
  • 垃圾收集堆栈框架

10.1 教程结论

欢迎阅读“ 使用LLVM实现语言 ”教程的最后一章。在本教程中,我们将我们的小万花筒语言从无用的玩具发展成为一个半有趣(但可能仍然无用)的玩具。:)

有趣的是看到我们走了多远,以及它采取的代码有多少。我们构建了整个词法分析器,解析器,AST,代码生成器,交互式运行循环(带有JIT!),并在独立的可执行文件中发出调试信息 - 所有这些都在1000行(非注释/非空白)代码中。

我们的小语言支持几个有趣的特性:它支持用户定义的二元和一元运算符,它使用JIT编译进行即时评估,并且它支持一些带SSA构造的控制流构造。

本教程的部分想法是向您展示定义,构建和使用语言是多么容易和有趣。构建编译器不一定是一个可怕的或神秘的过程!既然您已经看过一些基础知识,我强烈建议您接受代码并进行破解。例如,尝试添加:

全局变量 - 虽然全局变量在现代软件工程中具有问题价值,但在将诸如Kaleidoscope编译器本身之类的快速小工具组合在一起时,它们通常很有用。幸运的是,我们当前的设置使得添加全局变量变得非常容易:只需要进行值查找检查,以查看未解析的变量是否在拒绝它之前在全局变量符号表中。要创建新的全局变量,请创建LLVM GlobalVariable类的实例 。 类型变量 - Kaleidoscope目前仅支持double类型的变量。这使得语言非常优雅,因为只支持一种类型意味着您永远不必指定类型。不同的语言有不同的处理方式。最简单的方法是要求用户为每个变量定义指定类型,并在符号表中记录变量的类型及其Value *。 数组,结构,向量等 - 添加类型后,您可以开始以各种有趣的方式扩展类型系统。简单数组非常简单,对许多不同的应用程序非常有用。添加它们主要是学习LLVM getelementptr指令如何工作的练习:它是如此漂亮/非常规,它有自己的FAQ! 标准运行时 - 我们当前的语言允许用户访问任意外部函数,我们将它用于“printd”和“putchard”之类的东西。当您扩展语言以添加更高级别的构造时,如果将这些构造降低到调用语言提供的运行时,这些构造通常最有意义。例如,如果将哈希表添加到语言中,则将例程添加到运行时可能是有意义的,而不是一直将它们内联。 内存管理 - 目前我们只能访问Kaleidoscope中的堆栈。能够通过调用标准libc malloc / free接口或垃圾收集器来分配堆内存也很有用。如果您想使用垃圾收集,请注意LLVM完全支持精确垃圾收集,包括移动对象并需要扫描/更新堆栈的算法。 异常处理支持 - LLVM支持生成零成本异常,这些异常与其他语言编译的代码互操作。您还可以通过隐式使每个函数返回错误值并进行检查来生成代码。您还可以明确使用setjmp / longjmp。这里有很多不同的方式。 面向对象,泛型,数据库访问,复数,几何编程,… - 真的,你可以添加到语言中的疯狂功能没有尽头。 不寻常的域 - 我们一直在谈论将LLVM应用于许多人感兴趣的域:为特定语言构建编译器。但是,还有许多其他域可以使用通常不被考虑的编译器技术。例如,LLVM已被用于实现OpenGL图形加速,将C ++代码转换为ActionScript,以及许多其他可爱和聪明的东西。也许你会成为JIT第一个使用LLVM将正则表达式解释器编译成本机代码的人? 玩得开心 - 尝试做一些疯狂和不寻常的事情。像其他人一样建立一种语言,就像尝试一些有点疯狂的东西或看到它的结果并没有那么有趣。如果您遇到困难或想要谈论它,请随时给llvm-dev邮件列表发送电子邮件:它有很多人对语言感兴趣并且经常愿意提供帮助。

在我们结束本教程之前,我想谈谈生成LLVM IR的一些“技巧和窍门”。这些是一些可能不明显的更微妙的东西,但如果你想利用LLVM的功能,它们非常有用。

10.2 LLVM IR的属性

我们在LLVM IR表格中有一些关于代码的常见问题 - 让我们现在就把这些问题解决掉,好吗?

10.2.1 目标独立性

万花筒是“便携式语言”的一个例子:用Kaleidoscope编写的任何程序在运行它的任何目标上都会以相同的方式工作。许多其他语言都有这个属性,例如lisp,java,haskell,javascript,python等(请注意,虽然这些语言是可移植的,但不是所有的库都是)。

LLVM的一个不错的方面是它通常能够在IR中保持目标独立性:您可以将LLVM IR用于Kaleidoscope编译的程序,并在LLVM支持的任何目标上运行它,甚至可以在目标上发出C代码并对其进行编译LLVM本身不支持。您可以轻而易举地告诉Kaleidoscope编译器生成与目标无关的代码,因为它在生成代码时从不查询任何特定于目标的信息。

LLVM为代码提供紧凑的,与目标无关的表示这一事实让很多人兴奋不已。不幸的是,这些人在询问有关语言可移植性的问题时,通常会考虑C语言或来自C家族的语言。我说“不幸的是”,因为除了传送源代码之外,实际上没有办法让(完全通用的)C代码可移植(当然,C源代码实际上也不是可移植的 - 所以端口真的很旧应用程序从32位到64位?)。

C的问题(再次,完全普遍性)​​是它充满了目标特定的假设。作为一个简单的例子,预处理器在处理输入文本时经常破坏性地从代码中删除目标独立性:

  1. #ifdef __i386__
  2. int X = 1;
  3. #else
  4. int X = 42;
  5. #endif

虽然可以为这样的问题设计越来越复杂的解决方案,但是它不能以比提供实际源代码更好的方式完全解决。

也就是说,有一些有趣的C子集可以移植。如果您愿意将原始类型修复为固定大小(例如int = 32位,long = 64位),请不要关心ABI与现有二进制文件的兼容性,并且愿意放弃其他一些小功能,你可以有便携式代码。这对于诸如内核语言之类的专用域是有意义的。

10.2.2 安全保障

上面的许多语言也是“安全”语言:用Java编写的程序不可能破坏其地址空间并使进程崩溃(假设JVM没有错误)。安全性是一个有趣的属性,需要结合语言设计,运行时支持和操作系统支持。

当然可以在LLVM中实现安全语言,但LLVM IR本身并不能保证安全。LLVM IR允许不安全的指针强制转换,在释放错误之后使用,缓冲区溢出以及各种其他问题。安全性需要在LLVM之上作为一个层实现,并且方便地,几个小组已经对此进行了调查。如果您对更多细节感兴趣,可以在llvm-dev邮件列表上询问。

10.2.3 语言特定优化

LLVM关闭许多人的一件事是,它并没有在一个系统中解决所有世界的问题(抱歉’世界饥饿’,其他人将不得不在其他日子解决你)。一个具体的抱怨是人们认为LLVM无法执行高级语言特定优化:LLVM“丢失了太多信息”。

不幸的是,这真的不是给你一个完整统一版本的“Chris Lattner的编译器设计理论”的地方。相反,我会做一些观察:

首先,你是对的,LLVM确实会丢失信息。例如,在撰写本文时,无法在LLVM IR中区分SSA值是来自ILP32机器上的C“int”还是C“long”(调试信息除外)。两者都被编译成’i32’值,并且有关它的来源的信息丢失了。这里更普遍的问题是LLVM类型系统使用“结构等价”而不是“名称等价”。令人惊讶的另一个地方是,如果你在高级语言中有两种具有相同结构的类型(例如,两个具有单个int字段的不同结构):这些类型将编译成单个LLVM类型,这将是不可能的告诉它是什么来的。

其次,虽然LLVM确实丢失了信息,但LLVM并不是固定的目标:我们会以多种不同的方式继续增强和改进它。除了添加新功能(LLVM并不总是支持异常或调试信息)之外,我们还扩展了IR以捕获用于优化的重要信息(例如,参数是符号还是零扩展,有关指针别名的信息等)。许多增强功能都是用户驱动的:人们希望LLVM包含一些特定的功能,因此他们继续并扩展它。

第三,可以轻松添加特定于语言的优化,并且您可以选择多种方式进行优化。作为一个简单的例子,很容易添加特定于语言的优化过程,这些过程“了解”为语言编译的代码。在C系列的情况下,有一个“知道”标准C库函数的优化过程。如果在main()中调用“exit(0)”,它知道将其优化为“return 0;”是安全的,因为C指定了“exit”函数的作用。

除了简单的库知识外,还可以将各种其他语言特定信息嵌入到LLVM IR中。如果您有特殊需求并碰壁,请将主题放在llvm-dev列表中。在最糟糕的情况下,您可以始终将LLVM视为“哑代码生成器”,并在特定语言的AST上实现您在前端所需的高级优化。

10.3 提示和技巧

在使用LLVM后,您可以了解到各种有用的提示和技巧,这些提示和技巧乍一看并不明显。本节不是让每个人重新发现它们,而是讨论其中的一些问题。

10.3.1 实现便携式offsetof /

如果您试图保持编译器生成的代码“目标独立”,那么有一件有趣的事情是,您经常需要知道某些LLVM类型的大小或llvm结构中某些字段的偏移量。例如,您可能需要将类型的大小传递给分配内存的函数。

不幸的是,这可以在不同的目标之间变化很大:例如,指针的宽度通常是针对特定目标的。但是,有一种 聪明的方法可以使用getelementptr指令 ,允许您以可移植的方式计算它。

10.3.2 垃圾收集堆栈框架

有些语言希望显式管理它们的堆栈帧,通常是为了对它们进行垃圾收集或者允许轻松实现闭包。实现这些功能通常比显式堆栈帧更好,但LLVM确实支持它们, 如果你愿意的话。它需要您的前端将代码转换为 Continuation Passing Style和尾调用(LLVM也支持)。