编译器和 IR:LLVM IR、SPIR-V 和 MLIR
编译器通常是各种开发工具链中的关键组件,可提高开发人员的工作效率。编译器通常用作独立的黑匣子,它使用高级源程序并生成语义上等效的低级源程序。不过,它仍然是内部结构倾向的;内部之间流动的内容就称为中间表示 (IR)。
IR 对编译器至关重要。就像有许多编译器一样,也有许多 IR 在使用中。到目前为止,我很幸运能与三个 IR 有直接或简介的开发经验——分别是LLVM IR、SPIR-V、MLIR,尤其是最近两个工作,在前几年就开始了调研。因此,我想写一系列博客文章来记录我对编译器和 IR 的理解。希望它能对其他人有益。
这是第一个,我将概述编译器和 IR 开发的总体趋势。在我看来,重点是解释为什么这些现有的 IR 是目前的方式,而不是确切的机制是什么以及如何使用它们。通过各种语言规范和教程可以更好地提供内容和方式。因此,这不可避免地意味着讨论将是抽象的和哲学的,或者用最近的时髦术语来说是原型化的。 我将在后面的博客文章中更具体。 😉此外,我将尝试以比较的方式解释它们,因为通常它通过锚定一些现有概念来更容易掌握新概念。
顺便说一句,我可能会在文章中讨论编译器以外的领域。但我保证它们肯定有联系。
Without further ado—
编译器和 IR
在深入研究具体的 IR 之前,先讨论一下编译器和 IR。
抽象和语义
尽管技术取得了惊人的进步,但自文明以来,人类的大脑几乎没有变化。我们的理解几乎无处不在的日益增长的复杂性的方式是通过抽象。抽象帮助我们忽略次要因素并专注于主要方面。它减少了人脑需要处理的变量数量,这是必不可少的。
从抽象问题中得出的东西通常称为模型,例如,用于以抽象方式描述计算机的机器模型(在编程语言中),用于描述一个数据如何与另一个数据相关的数据模型(在数据系统中),用于描述时序和故障假设的系统模型(在分布式系统中)。
模型通常是对原始实体的理想描述。模型有时与现实相去甚远,比如,很难为股票市场提出一个模型。尽管如此,它们对于将复杂性保持在特定级别是必不可少的——模型通常会为我们提供清晰的语义,这些原则必须根据定义成立。这使我们能够以逻辑或数学方式推理我们正在建模的实体。人类的大脑只是渴望逻辑性和可解释性。
看来我在这里跑题了;但实际上,这是在试图解释编译器开发讨论中的术语,我们将在更广泛的意义上看到很多术语——抽象、模型、语义和推理。
计算机科学的其他领域也关心这些方面,比如,我们希望编写具有清晰抽象的面向对象类。但很多时候,艺术比科学更多。另一方面,编译器更多地从科学的角度关注抽象;他们想要清晰的语义,这样他们就可以证明他们所做的转换是正确的,而不是源程序。这就引出了。。。
正确性和优化
编译器最关心的是正确性;优化始终是第二位的。如果生成“高性能”代码不遵循原始程序的意图,那么它在任何意义上都无关紧要。
如果没有明确的语义,就无法定义和商定正确性。未知操作束缚了编译器的手,唯一安全的选择就是对它们不做任何事情。需要通过编译器内部的完整转换流程来保持正确性。因此,编译器内部存在“边界”:在每个转换步骤(通常称为传递)中,可能会存在某种形式的冲突,但在它之后,生成的代码应该是正确的。编译器严重依赖验证来检查转换步骤后的正确性。
只有在有了正确性之后,我们才能谈论优化和性能。生成高性能代码似乎是一个明确的目标;但是,仍然有很多微妙之处。
不同的源代码具有不同的编程模式,不同的硬件有利于不同的指令序列。编译器位于源代码和目标硬件连接之间,在性能方面的选择确实有限,因为它需要在许多因素之间进行权衡,以确保优化转换在大多数情况下确实是有益的。这是一个非常高的门槛,特别是对于处理通用编程语言(如 C++)的编译器。所以最终,只有相当多的转换(如死代码消除、常量折叠、规范化等)可以普遍运行;许多其他转换被推到第二个特定于目标的优化阶段。
当程序流经编译器层时,越来越多的高级信息将分解为低级和详细的指令,这称为降低。相反的方式称为提升,这通常要困难得多,因为它试图从混乱的细节中找出大局。降低是编译器内部的正常流程。因此,我们可以看到,由于缺少高级信息,在结构上发生在后期阶段的转换具有缺点。这限制了特定于目标的优化可以执行的操作。
这背后的根本问题是不同应用领域(特别是使用通用编程语言)之间以及编译器内部不同垂直转换路径(尤其是使用相同的通用 IR)之间的强耦合。
解耦是实现更复杂系统和高级用例的通用方法,正如在许多领域(如果不是全部)所看到的那样。展望未来,拥有特定于领域的语言和解耦的编译器内部结构将更好地释放优化能力。稍后讨论 MLIR 时会详细介绍这一点。
生产力工具
这可能已经很明显了,但重申一下,编译器确实是提高开发人员生产力的工具。从技术上讲,直接编写汇编可能看起来很奇怪和很酷;这几乎不是一种富有成效的方法(但如果做得好,它是一种完全有效的方法,可以获得最佳性能)。能够用更高级的语言进行编写,使开发人员无需考虑芯片上的寄存器和指令,这些寄存器和指令繁琐、耗时且容易出错。这极大地提高了生产力。
现在,我们谈到了管理日益增长的复杂性的方法——我们在不同层次上都有抽象;我们只需要构建工具来自动化不同层次的抽象之间的转换。
并非所有此类转换都可以自动化;但对于那些可能的人,我们可以将他们的转换工具视为更广泛意义上的编译器。例如,在数据处理系统中,Apache Beam 为描述数据处理作业提供了统一的抽象,并将它们转换为底层执行引擎(如 Spark 或 Flink)提供的任何抽象,然后最终编译为具体机器上的任务和编排。这里的完整流程可以看作是编译。
类似编译器的工具的存在通过将细节隐藏在较低级别并让开发人员专注于问题的更高级别描述,大大提高了开发人员的工作效率。编译器通过在 IR 上运行一系列转换来自动转换抽象级别并弥合差距。
IR 形式和兼容性
顾名思义,IR 只是程序的表示形式。它们旨在使转换更容易。(嗯,大多数情况下,正如我们稍后将看到的那样,它们可以实现其他目的。
在早期,我们可以为所有编译器内部使用一个单一的 IR,但随着编译器的发展和工具链变得越来越复杂,界限变得模糊:现在编译器可以在内部具有多个级别的表示(例如,通过 Clang 编译的 C++ 程序通过 Clang AST、LLVM IR、MachineInstr、MC 等)。我们还可以看到完整的编译器被拆分为离线和在线阶段(例如,对于 GPU),两者之间的程序表示也可以称为 IR(例如,SPIR-V),尽管不再完全是内部的。
IR 可以有三种形式:用于高效分析和转换的内存形式、用于持久化和交换的字节码形式,以及用于人工检查和调试的文本形式。现在,根据用例,关于哪种形式是以中心为中心以及支持多少兼容性,这里有一些有趣的设计权衡。例如,LLVM IR 旨在供编译器内部使用,因此它选择高效处理内存形式和弱兼容性,而 SPIR-V 旨在使用硬件驱动程序,因此它选择快速处理字节码形式和强兼容性。没有对错之分;这是一个满足需求的问题。但它确实极大地影响了建立在它们之上的整个生态系统。
IR 设计注意事项
没有通用的规则,我们肯定会设计出好的 IR。大多数时候,它是权衡不同的权衡并做出选择。这种权衡涉及当前情况的常见或特殊程度、它给整个编译器堆栈带来的组合成本、对转换的影响等等。
但作为一般准则:
- IR 中的操作需要具有清晰的语义。如前所述,这是正确性的基础。
- 然后,如果可能的话,我们通常希望操作是正交的;这使得拥有规范形式成为可能,并减少了我们在编写转换时需要考虑的情况。
- 我们通常也不希望看到来自 IR 不同部分的重复信息,这在各种转换后存在不一致的高风险。
- 尽可能多地保留高级信息也是有益的,因为如果可能的话,降低然后在需要时提高是非常困难的。
- 其他的。
所有似乎都合理的指导方针,对吧?现在我实际上可以对其中的大多数提出反例:
- 如果硬件具有特殊的功能单元,我们希望将其公开,即使它在 IR 中引入了非正交操作。例如,除了 GPU IR 中常见的单独乘法和加法运算外,还经常看到融合的乘法加法运算。
- 在 SPIR-V 中,模块将首先声明所有需要的功能;该信息可以通过对 IR 进行分析来推断。所以这是重复的信息。但它节省了 GPU 驱动程序编译器的工作量,因为它不需要在运行时执行此类分析,从而提高了运行时效率。
- 为了保留高级信息,如果原始输入足够低(如 C),那么我们无事可做,只能提高级别,例如对标量源代码执行自动矢量化。
以上是为了强调 IR 设计充满了权衡,并且特定于领域和用例。如前所述,如果编译器试图为广泛的应用程序提供服务,而一个 IR 试图为不同的垂直转换流提供服务,那么目标冲突是很自然的。不同的需求使得很难在讨论中确定无争议的方向。这将意味着失去快速迭代和发展的能力,这有时可能会付出高昂的代价。
在这里,通过特定于领域并使用单独的 IR 级别/片段来拆分编译器会很有帮助。这允许每个域根据其特征对编译器产生最佳影响。不同级别/部分的 IR 可以只关注他们打算服务的用例,因此也可以做出最合适的选择。
LLVM IR
LLVM 最初于 2003 年发布。经过近20年的发展,它已经非常成熟,并拥有梦幻般的生态系统。支持许多前端编程语言和后端硬件目标。许多软件或硬件供应商发布了他们自己的 LLVM 修改分支来支持他们自己的堆栈。我认为LLVM对行业的重要性不需要任何阐述。
解耦和模块化编译器
LLVM带来的最重要的东西是解耦和模块化的实践。围绕 LLVM 构建的大量精彩库和工具只是自然而然的结果。
在LLVM之前的时代,编译器是相当临时的和单一的。这些编译器仍然可以像当前基于 LLVM 的编译器一样由三个阶段组成——用于解析源语言的前端、用于执行优化的优化器以及用于生成目标机器代码的后端,但它们通常只关注特定语言(系列)或目标硬件。不同的编译器堆栈共享的很少。由于紧密耦合和整体性质,不可能利用编译器堆栈中的现有前端/后端支持并插入新的前端/后端支持。这阻碍了编译逻辑的可重用性,因此没有真正实现三阶段编译器所承诺的可重定位性。
LLVM 通过解耦彻底改变了上述内容。它的核心显然是 LLVM IR,它具有使用控制流和包含 SSA 形成指令的基本块来表示程序的全部功能。完整性使它能够自包含,因此它可以与其他表示分离,并作为前端和后端之间的唯一中间交换。这将前端和后端完全解耦。
然后,我们所需要的就是执行模块化的良好实践。LLVM 代码库被组织为一组库。诚然,图书馆也有自己的问题;但它们作为系统级模块化的方式经过了实战测试。库在编译器组件之间设置了相对清晰的边界。通过公开适当的 API,它还允许人们选择和混合感兴趣的编译器功能,并通过调用 Clang 库来执行各种任务,例如静态分析和格式化。所有这些都非常有用。(想想争论样式和手动格式化代码所节省的时间 clang-format !
纹理 IR forms
除了解耦和模块化之外,LLVM IR 还引入了许多其他可用性和生产力改进。除了内存形式之外,对文本 IR 形式的原生支持将传统的 UNIX 理念带回了编译器——让每个工具都做一个简单的工作,并将它们与文件和管道链接在一起。
在类 UNIX 系统中,文件是资源的通用抽象。特别是文本文件,是大多数工具交换信息的媒介。它们功能强大,足以支持不同的需求,而且使用起来很直观。在处理管道(例如, cat | cut -f2 | sort | uniq -c )的中间转储纹理表示以查看或调试中间状态是很自然的。从长远来看,没有什么比简单更重要了!
很难说编译器组件,即使是模块化的,也是简单的工具。但是,纹理 LLVM IR 表单确实服务于 UNIX 文件的目的,使链接工具和检查内部状态变得简单。这包括使用 FileCheck 测试编译器本身,因为现在我们只需输入人类可读的纹理形式,并检查输出的纹理形式。
The other side of the coin
LLVM 是编译器开发的一大飞跃。凭借其良好的设计和充满活力的开源社区的努力,我们已经看到许多出色的工具应运而生,并提高了开发人员的生产力。然而,据说每枚硬币都有两面。现在,以现有的LLVM生态系统为基础,设计权衡所投射的阴影变得越来越明显。
集中化和forks, forks, forks
LLVM IR 以整个 LLVM 生态系统为中心。这是前端和后端大解耦的基础。但是,这也意味着完整的流量必须通过 LLVM IR。
由于其重心的性质,改变 LLVM IR 本身需要满足一个非常高的标准。所有工具都应该处理它,并且有如此多的转换,不同组织的工作流程都经过它。即使您不需要通过该频率的整个流量,调整 IR 的一小部分仍然会引发令人惊讶的涟漪效应。因此,这自然意味着变化是缓慢的,需要许多利益相关者的广泛讨论和签署。这是保证 LLVM IR 质量的必要条件;但是,如果我只是有一个非常孤立的需求,那么就很难激发变革并证明其落地的合理性。
一种方法,也是典型的方法,就是分叉 LLVM 并将修改保留在本地。但这也要付出高昂的代价。LLVM IR 以中心为中心确实尽可能地支持上游。monorepo 中每天有近 100 个提交土地,带来了各种新功能和错误修复。如果不始终如一地合并,它就会有越来越大的分歧风险,然后最终变得无法管理,除非过时是好的。另一方面,持续追赶上游意味着专门的团队和工程工作。
因此,最终的结果是,我们在不同的版本或提交中拥有许多不同风格的 LLVM 分支,具有不同程度的新鲜度。如果在全球范围内进行维护和更新这些分支,肯定会花费大量的工程工作。
虽然很难说这个问题是LLVM独有的;由开源社区开发并由各种组织生产的大型复杂系统也存在类似的问题。但是,这些项目通常需要某种定制;相比之下,LLVM IR 的绝对中心角色使其变得困难。从某种意义上说,这是一种强大的耦合。MLIR在解耦IR方面又迈出了一步。
演变和兼容性
LLVM IR 的另一个设计选择是通过各种分析和转换来共同发展 IR。这对于越来越好的工具链至关重要。但这确实意味着弱兼容性保证。社区将尝试尽可能地保持兼容性,但肯定会保留进行重大更改的能力。
编译器通常在接近操作系统和硬件设备的级别上运行。因此,人们利用 LLVM IR 作为设备驱动程序的程序表示是很自然的,特别是考虑到强大的生态系统以及 LLVM IR 具有字节码形式这一事实!
但是,使用 LLVM IR 作为表示形式来流经一组连贯的软件和工具是支持良好的路径;如果涉及硬件和设备,那就另当别论了。硬件设备可能存在于某些最终产品(例如,手机)中,因此由设备制造商和最终消费者控制;因此,无法保证包含使用 LLVM IR 的 LLVM 库的驱动程序何时更新(如果有的话)。
以这种方式使用 LLVM IR 的尝试很多,但成功与否参差不齐。一个值得注意的例子是标准可移植中间表示 (SPIR)。SPIR 旨在代表 OpenCL 内核。它是固定在特定版本的 LLVM IR,OpenCL 计算结构定义为 LLVM 内部函数和元数据。多年来,Khronos Group 逐渐意识到 LLVM IR 并不是真正为此类任务而设计的,这导致了 SPIR-V 的诞生。
SPIR-V
SPIR-V 最初于 2015 年发布。它旨在成为多个 Khronos API 的中间语言,包括 Vulkan、OpenGL 和 OpenCL。定义一个新的 IR 并围绕它构建完整的软件堆栈是一项巨大的工作。尽管如此,由于特定的领域和用例,SPIR-V 被认为是值得的。
标准性、可扩展性和兼容性
Khronos Group 的口号是“将软件连接到芯片”,这实际上是对其工作内容的相当简洁和准确的总结。连接是通过标准和 API 建立的。
Khronos Group 为硬件供应商定义了要实施的标准和 API,并为软件供应商定义了针对硬件的标准和 API,无论平台和设备如何。著名的 Khronos 标准包括 Vulkan、OpenGL、OpenCL;但还有更多。
标准的主要目的是在不同的实现之间提供抽象和一致性。但是,标准还应该能够公开特定于供应商的功能,以承认现实中的差异,并让软件在特定实现中获益最多。通过对功能的分层支持以及提出、升级和/或弃用功能的明确过程来满足相互竞争的需求。
SPIR-V是一个标准,所以也不例外。除了核心功能外,SPIR-V 还提供了许多用于扩展 IR 的机制,包括添加枚举令牌值、引入扩展或特定命名空间下的完整扩展指令集。值得注意的是,扩展还具有多个层 - 特定于供应商的层、EXT 层、KHR 层。任何供应商都可以提出扩展,但接近核心的扩展层需要更多的供应商加入,并经过更严格的审查和批准程序。SPIR-V 及其位于 Khronos 的管理工作组为分层功能支持提供了技术和组织框架。
LLVM IR 确实支持扩展 IR 的方法,特别是使用内部函数和元数据,但很难想象支持各种特定于供应商的内部函数,更不用说将特定于供应商的类型或特定于供应商的模式引入核心 LLVM IR 指令了。
此外,标准桥接软件和硬件非常强调稳定性和兼容性,因为硬件驱动程序的更新频率远低于软件工具链。
看到驱动程序在设备的生命周期内从未更新也就不足为奇了,例如,低端 Android 手机上的许多 Vulkan 驱动程序都停留在将近 7 年前发布的 Vulkan 1.0 上。如果我们使用 LLVM IR 作为表示,我们很有可能生产者使用最新的 LLVM 版本,但设备驱动程序中的消费者仍然使用多年前的版本。这可能会导致各种问题和头痛。相比之下,SPIR-V 提供了必备的稳定性和与版本和扩展机制的兼容性,以及稳定的二进制编码。
稳定的二进制形式
完整的编译器分为两个阶段:离线阶段,开发人员从一些高级源代码生成 SPIR-V,以及驱动程序进一步将 SPIR-V 编译为机器代码的在线阶段。
虽然和 LLVM 一样,SPIR-V 在这个编译流程中也是“中间”的,但它更关注驱动程序消耗的效率,因为这是在运行时发生的步骤(所以在线)。因此,SPIR-V 主要是一种二进制格式,编码具有设计选择,使驱动程序使用更容易,例如,预先声明所需的硬件功能,以节省驱动程序编译器运行繁重的分析来推断。没有定义的内存形式或纹理形式;这取决于实现 SPIR-V 标准的特定工具链。例如,SPIRV-Tools 定义了自己的内存表示和文本形式;MLIR 中的 SPIR-V 方言也是如此。
Serving the GPU domain
好的,我已经谈了很多关于“标准”和“便携式”部件的问题,但到目前为止还没有真正触及 SPIR-V 的 IR 方面。😊
坦率地说,IR 方面与 LLVM IR 没有太大区别。实际上,SPIR-V从LLVM IR中汲取了很多灵感(为什么不呢!—它还采用控制流图,其中包含包含 SSA 形成指令的基本块。这些指令的粒度类似于 LLVM IR。
不过,SPIR-V 的特别之处在于它原生支持许多 GPU 概念和内部函数,包括装饰、内置和指令等结构(例如,用于计算导数和采样纹理)。此外,为了满足图形和计算用途,有许多执行模型和模式。哦,当然,如果用于图形,则对结构化控制流要求。
作为以 GPU 为中心的标准,需要对 GPU 概念的原生支持、分层可扩展性、稳定且兼容的二进制格式。这些要求不符合 LLVM IR 的假设和权衡,因此设计了 SPIR-V。
但设计 IR 只是一个开始,构建完整的编译器工具链需要大量的工程工作。由于 SPIR-V 编译器堆栈与 LLVM IR 完全分离,因此无法利用现有的 LLVM 库。所以它从头开始,从汇编器、反汇编器,再到编译器和优化器。如果我们有一个可以帮助构建特定领域的编译器的基础设施,那就简单多了——
MLIR
MLIR 于 2019 年底登陆 LLVM monorepo。所以它只有 2 岁。我的感觉是,至少需要 5 年时间才能看到一个相当成熟的生态系统。因此,从这个意义上说,MLIR还很年轻,还有很多发展要做。但是,MLIR 已经为编译器带来了许多新的想法或深刻的变化,特别是基础设施化,以进一步解耦编译器和 IR。
基础设施建设
基础设施建设是技术发展的自然终点。作为基础架构的一部分意味着该解决方案足够成熟并得到广泛部署。在此基础上,下一次技术演进可能会发生。我们在交通、电力、互联网、公共云等领域都看到了这一点。这些肯定是巨大的;对于规模较小的技术也是如此,因为它有助于分担开发基础设施的成本,并让彼此专注于核心业务逻辑。
许多人将 MLIR 作为实现机器学习编译器的一种方式。服务于 ML 领域和强大的 ML 编译器确实是最初的应用,并且仍然是 MLIR 的一个重要角色;但MLIR远不止于此。
MLIR 是一种编译器基础结构,旨在提供可重用和可扩展的基本组件,以促进构建特定于领域的编译器。与 LLVM IR 或 SPIR-V 不同,我们有一个中央 IR,其中包含一整套指令来表示它们旨在编译的 CPU/GPU 程序,而在 MLIR 中,没有一个 IR 明显位于中心。
MLIR 提供的只是定义操作(广义上的指令)并根据功能形成操作的逻辑组(在 MLIR 中称为方言)的基础设施。MLIR 还尝试提供通用模式或传递,这些模式或传递可以只应用于合适的操作,而无需对其进行硬编码。
这两个目标都要求 MLIR 以更细粒度的方式查看编译器,以进一步分解概念。粒度不是将原子上的操作视为基础结构,而是向下延伸到类型、值、属性、区域和接口(如属性/类型/操作接口)。
操作可以具有任意数量的操作数、结果和属性,并且可以包含任意数量的区域。区域是一种强大的机制,它允许嵌套级别的操作并使信息本地化,从而简化分析和转换。操作还实现某些接口。这允许编写模式和传递以在接口上操作,因此与具体操作分离。
MLIR 中的所有概念都是抽象的,并且在设计上是分离的,以便映射到各种领域和用例。
Dialects, dialects, dialects
但基础结构的目的是帮助构建产品(这里是特定于领域的编译器)。我们通过调用 STL 甚至更高级别的库来编程 C++。很少有人会从头开始写所有东西。此外,基础设施也需要与其产品共同发展,因为需求无处不在。因此,在 MLIR 中,也有用于不同级别的抽象的树内方言。它们就像 C++ 的 STL。
MLIR的方言生态系统仍处于有机增长阶段,但已经有稳定和成熟的早期迹象。例如,我们将 LLVM 和 SPIR-V 作为边缘方言,以便在其他系统中与 IR 相互转换。(仅这一事实实际上就意味着 MLIR 实际上是一个基础设施,因为它能够完全定义其他 IR!我们有中间层次的抽象,如Linalg、Tensor、Vector、SCF,用于结构化代码生成。我们有用于低级计算的仿射、数学、算术。TensorFlow、TFLite、MHLO、Torch、TOSA 等 AI 框架方言,用于将 ML 模型图导入 MLIR。还有很多其他的。
Alex 在 MLIR 中发布了一张很棒的图表和不同方言的讨论,如果您有兴趣,非常值得一读。我稍后还会写下我对方言生态系统的理解。这些方言(以及最终连接和包装它们的部分或完整流程)将使开发特定于领域的编译器变得更加简单。
进一步拆分编译器和 IR
方言的基础设施化和过多实际上是朝着编译器和 IR 解耦和模块化迈出的又一步。
现在,我们不再是一个单一的中央 IR,而是有许多可重复使用的“部分”IR,这些 IR 在逻辑上组织成 MLIR 方言。如果这些现成的操作不能满足您的需求,那么使用声明式操作定义新的操作并编写它们的验证也非常容易。因此,如果MLIR再过几年才能进一步成熟,我们可以想象一个世界,在这个世界里,开发特定领域的编译器意味着只为项目特定的操作定义边缘方言,并选择现有的中级或低级方言并将它们链接在一起,形成一个完整的流程!这当然比从头开始构建所有内容花费更少的精力。
此外,解耦使我们能够根据域的特定需求灵活地进行权衡。我们只能选择必要的部分 IR 来拼凑一个成熟的编译器;我们不需要像 LLVM IR 那样完整地获取完整的 IR 及其所有复杂性。扩展现有组件的功能也更简单,因为接口是连接操作和模式/传递的东西。我们既可以定义实现现有接口的新操作,以使模式和传递立即工作,也可以将新接口实际附加到现有操作,使其支持外部模式和传递。
换句话说,如果 LLVM IR 本质上是中心化的,并且有利于统一的编译器流程,那么 MLIR 基础设施及其方言生态系统本质上是去中心化的,并且有利于多样化的编译器流程。
技术的典型趋势是从单一的单体选项演变为丰富多样的选择。上层尤其如此,因为在那里我们更接近具体的业务需求,而这些需求本质上是多样化的。例如,想想我们有多少 Web 前端框架、分布式数据处理框架、ML 框架。
对于较低层,它一直相当稳定;我们只有少数几个主要的硬件架构、编译器和操作系统。但是,半导体发展的放缓和不断增长的计算需求也推动了这里的变化。很难仍然只依赖通用架构并针对所有领域进行优化。开发特定领域的端到端解决方案是一个有趣的出路。我们看到RISC-V在ISA层面开创了定制和模块化的先河;MLIR 有效地定制和模块化了编译器和 IR。看看他们如何共同努力推动低技术层的前沿,这将是非常有趣的。
跨越系统边界的渐进式降低
在结束本节之前,我还想谈谈另一个方面。我们可以从两个方面来看待MLIR带来的分拆:
在水平方向上,方言将完整的 IR 拆分为同一级别的单独部分 IR,例如标量运算、向量运算、控制流运算等。在垂直方向上,MLIR 使方言和操作能够在不同的抽象级别上对概念进行建模。
这对于特定于域的编译器非常有用,这些编译器通常具有一个非常高级的源程序,该程序以声明性方式描述作业,并且需要编译为命令式机器代码。一步到位是无法控制的。最好使用多级抽象执行渐进式降低,因为它将关注点分开,并让每一层专注于一个专用任务。同样,解耦是使复杂系统易于处理的关键。
但这肯定不是全新的,因为我们在各种项目中已经有很长一段时间了类似 IR 的结构,比如 Clang AST 和 ML 框架图。非常强大的是,MLIR 允许使用相同的基础设施来表示不同的级别;这样不同层次之间的流动就可以变得无缝。
开发现代复杂系统的主要任务之一实际上是选择各种子系统并将它们链接在一起。子系统之间的边界设置了刚性屏障。大部分工作都花在从上一个子系统中获取输出,验证、改变,然后输入到下一个子系统中。现在,如果所有系统都使用相同的内部表示和基础设施,它将完全节省这项工作。更重要的是,由于相同的思维方式和工具,它还将使跨团队跨项目的协作变得更加容易!
结束语
好吧,这篇博文已经变得很长了。因为我试图捕捉我对编译器和 IR 及其发展趋势的整体理解,所以它也是抽象的,有点自由的形式。希望它为我今后讨论更具体的机制奠定必要的基础。感谢您的跟进!最后的回顾:
抽象是人类处理世界复杂性的方式。编译器是开发人员的生产力工具,用于在不同的抽象级别之间自动转换。编译器最关心的是正确性;生成最佳代码是次之又一。正确性基于清晰的语义和验证。编译器通常可以轻松获得大部分性能,并且它节省了我们专注于最具影响力的部分的工程工作量。
LLVM 与 LLVM IR 和库的解耦和模块化编译器。它还为具有纹理形式的编译器带来了 UNIX 的简单性。然而,LLVM IR 中的某些设计选择使其不适用于某些领域,例如,它对稳定性和兼容性没有硬性保证,并且 LLVM IR 本身是一个单片中心红外。
SPIR-V 是一个广泛关注 GPU 领域的标准。它为可扩展性提供了技术机制和组织结构。它还提供了与稳定的二进制形式的强兼容性。
MLIR 通过将单体 IR 分解为可混合方言来进一步解耦编译器和 IR。它的基础设施释放了轻松定义和转换不同级别的抽象的力量。这与从单体到越来越模块化的总体方向相匹配,并让每个领域都有自己的定制解决方案,并有自己的权衡。希望将来编写特定于领域的编译器可以像选择、自定义和混合开放方言一样简单!