首先,由于这个答案涉及复杂和敏感的话题,我想几句免责声明:
- 我认为您的问题是关于 LLVM 和 GCC 的中端 IR(因为术语“LLVM IR”仅适用于中端)。讨论后端 IR(LLVM MachineIR 和 GCC RTL)和相关代码生成工具(LLVM Tablegen 和 GCC 机器描述)的差异是一个有趣且重要的话题,但会使答案扩大数倍。
- 我省略了 LLVM 的基于库的设计与 GCC 的整体设计,因为这与 IR 本身是分开的(尽管相关)。
- 我喜欢在 GCC 和 LLVM 上进行黑客攻击,而且我不会把其中一个放在首位。 LLVM 之所以如此,是因为人们可以从 GCC 在 2000 年代所犯的错误中吸取教训(从那时起,这些错误已经得到了显着改进)。
- 我很高兴改进此答案,因此如果您认为某些内容不准确或缺失,请发布 cmets。
最重要的事实是 LLVM IR 和 GCC IR(称为 GIMPLE)在它们的核心上并没有什么不同——它们都是基本块的标准控制流图,每个块都是 2 个输入、1 个输出指令的线性序列(所谓的“三地址码”)已转换为SSA form。自 1990 年代以来,大多数生产编译器一直在使用这种设计。
LLVM IR 的主要优点是它与编译器实现的绑定不那么紧密,定义更正式,并且具有更好的 C++ API。这使得处理、转换和分析变得更容易,这使其成为当今编译器和其他相关工具的首选 IR。
我将在下面的子章节中详细介绍 LLVM IR 的优势。
独立红外
LLVM IR 最初设计为可在编译器本身之外的任意工具中完全重用。 original intent 将其用于多阶段优化:IR 将因此在运行时由提前编译器、链接时优化器和 JIT 编译器进行优化。这并没有奏效,但可重用性还有其他重要含义,最明显的是它允许轻松集成其他类型的工具(静态分析器、仪器等)。
GCC 社区从不希望启用除编译器之外的任何工具(Richard Stallman 拒绝尝试使 IR 更可重用,以防止第三方商业工具重用 GCC 的前端)。因此 GIMPLE(GCC 的 IR)从未被认为是一个实现细节,特别是它没有提供编译程序的完整描述(例如,它缺少程序的调用图、类型定义、堆栈偏移量和别名信息)。
灵活的管道
可重用性和使 IR 成为独立实体的想法导致了 LLVM 中的一个重要设计结果:编译过程可以以任何顺序运行,从而防止复杂的过程间依赖关系(所有依赖关系都必须通过分析过程明确)和使编译管道的实验更容易,例如
更好的单元测试支持
独立 IR 允许 LLVM 使用 IR 级别的单元测试,从而可以轻松测试优化/分析极端情况。通过 C/C++ sn-ps(如在 GCC 测试套件中)实现这一点要困难得多,即使您管理,生成的 IR 很可能会在编译器的未来版本中发生显着变化,并且您的测试打算用于的极端情况将不再被覆盖。
简单的链接时优化
独立 IR 可轻松实现 combination of IR from separate translation units 并进行后续(整个程序)优化。这不是链接时优化的完全替代品(因为它不处理生产软件中出现的可扩展性问题),但通常对于较小的程序(例如嵌入式开发或研究项目)来说已经足够了。
更严格的 IR 定义
尽管criticized by academia,LLVM IR 与GIMPLE 相比,semantics 严格得多。这简化了各种静态分析器的实现,例如IR Verifier.
无中间 IR
LLVM IR 由前端(Clang、llgo 等)直接生成并保存在整个中端。这意味着所有工具、优化和内部 API 只需要在单个 IR 上运行。 GCC 并非如此 - 甚至 GIMPLE 也有三个不同的变体:
- 高级 GIMPLE(包括词法范围、高级控制流结构等)
- 预 SSA 低 GIMPLE
- 最终的 SSA GIMPLE 和 GCC 前端通常会生成中间 GENERIC IR 而不是 GIMPLE。
更简单的红外
与 GIMPLE 相比,LLVM IR 通过减少 IR 消费者需要考虑的案例数量而刻意简化。我在下面添加了几个示例。
显式控制流
LLVM IR 程序中的所有基本块都必须以显式控制流操作码(分支、goto 等)结尾。不允许隐式控制流(即失败)。
显式堆栈分配
在 LLVM IR 虚拟寄存器中没有内存。堆栈分配由专用的alloca 操作表示。这简化了堆栈变量的工作,例如不需要等效于 GCC 的 ADDR_EXPR。
显式索引操作
与 GIMPLE 有大量内存引用操作码(INDIRECT_REF、MEM_REF、ARRAY_REF、COMPONENT_REF 等)相反,LLVM IR 只有普通的加载和存储操作码,所有复杂的算术都移到专用的结构化索引操作码 @987654329 @。
垃圾回收支持
LLVM IR 为垃圾收集语言提供 dedicated pseudo-instructions。
高级实现语言
虽然 C++ 可能不是最好的编程语言,但它绝对允许编写更简单(在许多情况下更实用)的系统代码,
尤其是 C++11 之后的变化(LLVM 积极采用新标准)。继 LLVM 之后,GCC 也采用了 C++,但大部分代码库仍以 C 风格编写。
C++ 实现更简单代码的实例太多了,所以我只举几个例子。
显式层次结构
LLVM 中的运算符层次结构是通过标准继承和template-based custom RTTI 实现的。另一方面,GCC 通过旧式继承-通过-聚合实现相同的目标
// Base class which all operators aggregate
struct GTY(()) tree_base {
ENUM_BITFIELD(tree_code) code : 16;
unsigned side_effects_flag : 1;
unsigned constant_flag : 1;
unsigned addressable_flag : 1;
... // Many more fields
};
// Typed operators add type to base data
struct GTY(()) tree_typed {
struct tree_base base;
tree type;
};
// Constants add integer value to typed node data
struct GTY(()) tree_int_cst {
struct tree_typed typed;
HOST_WIDE_INT val[1];
};
// Complex numbers add real and imaginary components to typed data
struct GTY(()) tree_complex {
struct tree_typed typed;
tree real;
tree imag;
};
// Many more operators follow
...
和标记的联合范式:
union GTY ((ptr_alias (union lang_tree_node),
desc ("tree_node_structure (&%h)"), variable_size)) tree_node {
struct tree_base GTY ((tag ("TS_BASE"))) base;
struct tree_typed GTY ((tag ("TS_TYPED"))) typed;
struct tree_int_cst GTY ((tag ("TS_INT_CST"))) int_cst;
struct tree_complex GTY ((tag ("TS_COMPLEX"))) complex;
所有 GCC 运算符 API 都使用基本的 tree 类型,可通过 fat 宏接口(@987654338@、TREE_IMAGPART 等)访问。接口仅在运行时进行验证(并且仅当 GCC 配置了 --enable-checking 时)并且不允许静态检查。
更简洁的 API
LLVM 通常为优化器中的模式匹配 IR 提供更简单的 API。例如,检查指令是否是 GCC 中的常量加法看起来像
if (gimple_assign_p (stmt)
&& gimple_assign_rhs_code (stmt) == PLUS_EXPR
&& TREE_CODE (gimple_assign_rhs2 (stmt)) == INTEGER_CST)
{
...
在 LLVM 中:
if (auto BO = dyn_cast<BinaryOperator>(V))
if (BO->getOpcode() == Instruction::Add
&& isa<ConstantInt>(BO->getOperand(1))
{
任意精度算术
由于 C++ 支持重载,LLVM 可以使用任意精度整数进行所有计算,而 GCC 仍然使用物理整数(HOST_WIDE_INT 类型,在 32 位主机上为 32 位):
if (!tree_fits_shwi_p (arg1))
return false;
*exponent = tree_to_shwi (arg1);
如示例所示,这可能会导致错过优化。
几年前,GCC 已经获得了与 APInts 等效的版本,但大部分代码库仍然使用 HOST_WIDE_INT。