GCC IR和LLVM IR有何不同?

37

为什么人们更喜欢使用LLVM IR,它与GCC IR有何不同?目标依赖是否是一个因素?

我对编译器完全没有经验,在寻找答案的过程中花费了很多时间,但并没有找到任何相关信息。希望能够得到一些见解。


6
最大的实际区别是LLVM有一个可用的模块化API,而GCC开发者尽可能地确保不会暴露这样的API。这就是为什么如果你想要一个IR插件,LLVM是一个最佳解决方案的原因。 - SK-logic
1个回答

62
首先,由于本回答涉及复杂和敏感的主题,我想先做一些免责声明:
  • 我假设你的问题是关于LLVM和GCC的中间端IR(因为术语“LLVM IR”仅适用于中间端)。讨论后端IR(LLVM MachineIR和GCC RTL)以及相关的代码生成工具(LLVM Tablegen和GCC Machine Description)的差异是一个有趣且重要的话题,但这会使回答变得更加庞大。
  • 我忽略了LLVM基于库的设计与GCC单体设计之间的区别,因为这与IR本身是分开的(尽管相关)。
  • 我喜欢在GCC和LLVM上进行编程,并不会将其中一个放在另一个之前。LLVM之所以成为现在的样子,是因为人们可以从2000年代GCC的错误中学习到东西(并且自那时以来已经显着改进)。
  • 如果您认为某些内容不准确或缺失,请在评论中提出,我很乐意改进这个答案。
最重要的事实是LLVM IR和GCC IR(称为GIMPLE)在其核心方面并没有太大区别 - 两者都是基本块的标准控制流图,每个块都是一个线性序列的2个输入,1个输出指令(所谓的“三地址代码”),这些指令已转换为SSA形式。自1990年代以来,大多数生产编译器都使用了这种设计。
LLVM IR的主要优点是它与编译器实现的绑定不那么紧密,更加正式定义,并且具有更好的C++ API。这使得处理、转换和分析更加容易,这使得它成为当今编译器和其他相关工具的首选IR。
我将在下面的子章节中详细介绍LLVM IR的优点。
独立IR
LLVM IR最初的设计目的是在编译器本身之外完全可重复使用。原始意图是将其用于多阶段优化:IR将被静态编译器、链接时优化器和JIT编译器在运行时逐步优化。但这并没有奏效,但可重用性有其他重要的影响,最显着的是它允许轻松集成其他类型的工具(静态分析器、仪表器等)。
GCC社区从来没有希望除编译器之外启用任何工具(Richard Stallman反对使IR更具可重用性以防止第三方商业工具重用GCC的前端)。因此,GIMPLE(GCC的IR)从未被认为不仅仅是一种实现细节,特别是它没有提供已编译程序的完整描述(例如,它缺少程序的调用图、类型定义、堆栈偏移和别名信息)。
灵活的流水线
将IR作为独立实体的可重用性理念导致了LLVM中的一个重要设计结果:编译通道可以按任意顺序运行,这可以防止复杂的通道间依赖关系(所有依赖关系都必须通过分析通道明确说明),并使编译流程的实验变得更容易。
以下是翻译内容:
  • 每次编译后都运行严格的IR验证检查
  • 二分管道,以找到导致编译器崩溃的最小子集合
  • 模糊化传递顺序

更好的单元测试支持

独立的IR允许LLVM使用IR级别的单元测试,从而可以轻松测试优化/分析的边界情况。通过C/C++片段(如GCC测试套件)实现这一点要困难得多,即使您设法做到了,在未来版本的编译器中生成的IR也很可能会发生显著变化,您的测试旨在覆盖的边界情况将不再被覆盖。

简单的链接时优化

独立的IR使得易于组合来自不同翻译单元的IR,并进行后续(整个程序)优化。虽然这并不完全替代链接时优化(因为它无法处理生产软件中出现的可扩展性问题),但对于较小的程序(例如嵌入式开发或研究项目)通常已足够。

更严格的IR定义

尽管受到学术界的批评, LLVM IR相比于GIMPLE具有更严格的语义,这简化了各种静态分析器(例如IR Verifier)的实现。

没有中间IR

LLVM IR由前端(Clang、llgo等)直接生成并在整个中间层中保留。这意味着所有工具、优化和内部API只需要操作单个IR。对于GCC来说并非如此-即使是GIMPLE也有三种不同的变体:

  • 高级GIMPLE(包括词法作用域、高级控制流结构等)
  • 预SSA低GIMPLE
  • 最终SSA GIMPLE

而且,GCC前端通常会生成中间的GENERIC IR而不是GIMPLE。

更简单的IR

与GIMPLE相比,LLVM IR通过减少IR消费者需要考虑的情况数量,故意使其更简单。下面我添加了一些例子。

显式控制流

LLVM IR程序中的所有基本块都必须以显式控制流操作码(分支、转到等)结尾。不允许隐式控制流(即落空)。

显式堆栈分配

在LLVM IR中,虚拟寄存器没有内存。堆栈分配由专用的alloca操作表示。这简化了处理堆栈变量,例如不需要GCC的ADDR_EXPR的等价项。

显式索引操作

与GIMPLE相反,它有大量用于内存引用(INDIRECT_REF、MEM_REF、ARRAY_REF、COMPONENT_REF等)的操作码。LLVM IR仅具有纯负载和存储操作码,并且所有复杂算术都移至专用的结构化索引操作getelementptr中。

垃圾收集支持

LLVM IR为垃圾回收语言提供了专用的伪指令

更高级的实现语言

虽然C++可能不是最好的编程语言,但它确实允许编写更简单(在许多情况下更具功能性)的系统代码,特别是在C++11之后的变化下(LLVM积极采用新标准)。GCC也采用了C++,但大部分代码库仍以C风格编写。

有太多情况下C++使得代码更简单,因此我只列举一些。

明确的层次结构

LLVM中运算符的层次结构是通过标准继承和基于模板的自定义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宏接口(DECL_NAMETREE_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已经有了等效的APInt,但是大部分代码仍然使用HOST_WIDE_INT。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接