LLVM 3.0类型系统重写

原文地址:http://blog.llvm.org/2011/11/llvm-30-type-system-rewrite.html

LLVM 3.0中最为普遍的IR(因此编译器API)改变之一是完全重新实现LLVMIR类型。这个改变拖延了很久(最初的类型系统自LLVM1.0延续),它使得编译器更快,极大地简化VMCore的一个关键子系统,并消除了IR某些通常导致混淆与不方便的设计点。本文解释为什么要进行这些改变,以及新系统如何工作。

类型系统的目标

LLVMIR类型子系统是IR相当直接明了的部分。该类型系统包括3个主要部分:基本类型(像双精度及整数类型),派生类型(像struct,array以及vector),与处理类型前向声明(不透明)的一个机制。

类型系统有几个重要的要求约束其设计:我们希望能够使用高效的指针相等检查来确定结构化类型的相等,我们希望允许类型的晚期改进(比如在链接时,一个模块应该能够完成另一个模块中的类型),我们希望很容易表达许多不同的源语言,并且我们希望一个简单且可预测的系统。

一个IR类型系统唯一真正困难的部分是处理类型的前向声明。为了理解这,注意到类型系统设计师以类型的一个复杂图形来表示(通常是环状的)。例如,一个简单的整数单链表可能被声明成这样:

%intlist= type { %intlist*, i32 }

在这个情形里,类型图包括一个指向一个IntegerType与一个PointerType的StructType。而这个PointerType指回这个StructType。在许多真实的程序中,这个图是复杂且高度环化的,特别对于C++应用程序。

以这为背景,让我们从讨论LLVM 2.9及更早版本如何工作开始。

旧的类型系统

LLVM 2.9类型系统全由简单明了的片段组成,使用OpaqueType的一个实例来表示前向类型上面。当这个类型在后面被解析时(比如在链接时刻),一个称为“类型改进”的过程将所有指向旧的OpaqueType的指针更新为指向新的定义,在线修改类型图,然后删除原始的OpaqueType。例如,如果一个模块包含:

  %T1 = type opaque

  %T2 = type %T1*

那么T2是指向一个OpaqueType的一个PointerType。如果我们将%T1解析为{}(一个空的结构体),那么修改%T2为指向该空的StructType的一个PointType。更多关于这的信息,请参考LLVM2.9程序员手册的类型解析章节

修改的结果与移动类型

不幸地,尽管概念上简单,这个类型系统有几个问题。为了保证指针相等检查对结构化类型相等检查起作用,VMCore被要求,在类型解析期间一旦被修改,就要重新唯一化类型。这可能看起来不是一件大事,但单个类型的改进可以导致数以百计其他类型被改变,而改进成百或上千的类型相当常见(比如,在对LTO链接一个应用程序时)。这个性能是不可接受的,特别是因为唯一化环图要求完整的图同态检查,而我们之前的实现算法上效率不够。

另一个问题是不只是类型需要更新:任何包含指向要更新类型的指针东西,不然它会得到应该悬空指针。这个问题以各种方式被证实:例如每个Value有一个指向类型的指针。为了使系统更高效一点,Value::getType()实际上执行一个延迟“联合查找”(unionfind)步骤来确保它总是返回一个规范、唯一的类型。这使得Value::getType()(一个非常常用的调用)比实际代价更高。

这个“类型更新”问题导致的一个更糟的问题是在你通过LLVM API操作及构建IR时。因为类型可以移动,很容易得到悬空指针,导致许多混淆以及LLVMAPI大量损坏的客户端。要求使用类型改进来构建简单递归类型的事实使问题复杂化。我们尝试通过类PATypeHolder与PATypeHandle简化之,但它们仅在你正确使用时才起作用,而通常对它们的了解很少。

类型唯一化令人惊奇的行为

LLVM的许多客户端,一旦起作用,很快就会遭遇类型唯一化的一个令人惊奇的一面:类型名字不是类型系统的部分,它们是一个“暗地里的”数据结构。在类型唯一化期间不考虑名字,对一个类型你可以得到多个名字(这导致了大量的混淆)。例如,考虑:

  %T1 = type opaque

  @G1 = external global %T1*

 

  %T2 = type {}

  @G2 = external global %T2*

如果%T1后来被解析为{},那么%t1与%T2将都是相同空结构体类型的名字,之前称为“%T1*”的类型将与之前称为“%T2*”的类型统一,IR现在将输出为:

  %T1 = type {}

  @G1 = external global %T1*

 

  %T2 = type {}

  @G2 = external global %T1*

注意G2现在具有类型“%T1*”!这是因为类型系统中的名字只是一个暗地里的哈希表,因此asmprinter在输出时将在一个类型任意多的名字里选择其中一个。这是“正确的”,但令不知道类型系统底细的人非常困惑,不是有益的行为。它也使得阅读一个C++编译器的.ll倾印十分困难,因为许多结构上相同的类型具有不同的名字十分常见。

类型向上引用

最后一个(我不希望讨论太多的)问题是,之前我们会遇到一个情形,其中类型可以完全没有名字。尽管这从类型系统图角度没有问题,这不可能打印类型,如果它们是环状且无名。这个问题的解决方案是一个称为类型向上引用的系统。

类型向上引用是一个优雅的解决方案,它允许asmprinter(及parser)能够在有限的空间里,无需名字,表示任意递归的类型。例如,上面的%intlist例子可以表示为“{\2*,i32}”。它也允许构造某些良好(但令人惊奇)的类型,像“\1*”是一个指向自身的指针。

尽管有一定的美与优雅,类型向上引用从来没有被大多数人很好理解,导致了大量的混淆。能够从一个LLVMIR模块剥除名字(即-strip遍)是重要的,但编译器开发者能够理解这个系统也同样重要。

为新类型系统准备

带着所有这些问题,我意识到LLVM需要一个更新、更简单的类型系统。不过,新版LLVM能够读旧的.bc与.ll文件也同样重要。要启用这,庞大的重写被仔细地阶段化了:LLVM2.9的asmprinter被增强为将不透明(opaque)类型发布为序号化类型,而不是使用向上引用。因此,不是以向上引用发布%intlist例子,LLVM2.9将它发布为:

  %0 = type { %0*, i32 }

LLVM 3.0的计划是降低对LLVM 2.8(及更早版本)文件的兼容性,因此这使得LLVM3.0所需的“升级”逻辑变得简单得多。

LLVM 3.0类型系统

对大多数用户,LLVM3.0的新类型系统各方面很像2.9的类型系统。例如,由LLVM2.9产生的.bc文件与.ll文件可以由比特码读取器及.ll解析器读取并自动更新到3.0(虽然LLVM3.1将降低对2.9的兼容性)。这是因为类型系统保持了几乎之前的一切:基本与派生类型是相同的,仅移除了OpaqueType,增强了StructType。

简而言之,不是一个基于改进的,在内存中改变类型的类型系统(要求重新唯一化以移动/更新指针),LLVM3.0使用一个非常类似于C的类型系统,它基于类型完成(completion)。基本上,不是创建一个不透明类型,然后替换它,现在你创建一个没有主体的StructType,稍后指定其主体。要创建%intlist类型,现在你像这样编写:

  StructType *IntList =StructType::create(SomeLLVMContext, "intlist");

  Type *Elts[] = {PointerType::getUnqual(IntList), Int32Type };

  IntList->setBody(Elts);

这简明扼要,远好于2.9的方式。虽然对这个设计有几个不明显的衍生设计。

仅结构化类型可以递归

在之前的类型系统中,一个OpaqueType可以被解析为任意类型,允许像这样的古怪的东西“%t1= type %t1*”,这是一个指向自己的指针。在新类型系统里,仅IR结构化类型可以缺少主体,因此不可能创建一个不涉及struct的递归类型。

字面上的与被识别的结构体

在新类型系统里,实际上有两个不同的结构化类型:一个“字面上的”结构(比如“{i32,i32}”)以及一个“被识别的”结构(比如“%ty= type {i32, i32}”)。

被识别的结构是我们要讨论的种类:它们可以具有名字,且可以在创建类型后指定它们的主体。被识别结构不是唯一的,这是为什么它们由StructType::Create(…)产生。因为被识别类型潜在地是递归的,asmprinter总是使用名字输出它们(或者一个像%42的数字,如果这个被识别结构体没有名字)。

字面结构类型的作用类似于旧IR结构化类型:它们从来没有名字且是结构同一性唯一的:这意味着在构造时刻它们必须有主体元素,它们不能递归。在由asmprinter输出时,它们总是不带名字内联输出。字面结构化类型由StructType::get(…)方法创建,反映出它们是唯一的(这个调用可能也可能不实际分配一个新的StructType)。

我们期望被识别的结构化类型将是最普遍的,前端将仅在特殊的情形下产生一个字面结构化类型。例如,对元组,复数,及其他名字可以任意并使得IR更难以阅读的简单情形,使用字面结构化类型是合理的。优化器不在于哪个方式,因此如果你是前端的作者,尽管在你的IR倾印中使用你所喜欢看到的。

被识别的结构体与名字有一个1-1映射

之前类型名字被保存为一个“暗地里的”哈希表,现在它们是类型的一个固有函数部分,而且仅可被命名的类型才是被识别的结构体。这意味着LLVM3.0不会出现之前两个看上去不同的结构体以相同名字输出的,令人混淆的行为。当从一个模块剥除类型名时,被识别结构体只是变成匿名:它们仍然是“被识别的”,但它们没有名字。正如LLVMIR中其他匿名实体,它们以数值的形式被asmprinter输出。

在LLVMContext层面结构体名是唯一的

因为StructType::create总是返回一个新的被识别类型,当你创建两个同名的类型时,我们需要做些事情,解决方案是VMCore检测冲突,通过向类型添加后缀自动重命名后一个请求:当你请求一个“foo”类型时,你实际上可能得到类型名“foo.42”。这与其他IR对象像指令及函数是一致的,在LLVMContext层面,名字是唯一的。

链接器“链接”类型以及重新类型化IR对象

这个设计一个有趣的方面是它使得IR链接器的任务更复杂了一点。考虑当你将这两个IR模块链接起来时,发生什么:

x.ll:

  %A = type { i32 }

  @G = external global %A

y.ll:

  %A = type { i32 }

  @G = global %A zeroinitializer

链接器做的第一件事是将这两个模块载入同一个LLVMContext。因为两个名为“A”的类型必须是不同的类型,且因为只能有一个名为%A的类型,我们实际上在内存中得到这两个模块:

x.llmodule:

  %A = type { i32 }

  @G = external global %A

y.llmodule:

  %A.1 = type { i32 }

  @G = global %A.1 zeroinitializer

现在很清楚@G对象有不同的类型。在链接这两个全局变量时,现在取决于链接器将IR对象的类型重新策划为一组一致的类型,并重写为一致的状态。这要求链接器计算相同类型的集合,并解决VMCore过去惯常解决的图同态问题(如果有兴趣,参考lib/Linker/LinkModules.cpp中的remapType逻辑)。

将这个逻辑放入IR链接器,而不是VMCore,在许多层面上都优于之前的设计:现在这个合并与唯一化的代价仅由IR链接器承担,而不是由所有的比特码阅读代码及其他IR创建代码负担。代码更容易理解以及在算法上优化,因为我们只合并两个完整的图一次——而不是一次解析一个类型。最后,通过移出了某些复杂逻辑,缩小了VMCore的大小。

在优化器中(或更后)识别magic IR类型

正如在LLVM 2.9,类型名不是真正设计来在IR中用作语义信息:如果使用-strip遍从IR移除了所有外来的名字,我们期望一切都继续工作。不过,出于研究及其他目的,通过类型名字从前端向LLVMIR传播信息,有时是一个方便的实践。

在LLVM 3.0中这将可靠工作(只要你不运行strip遍或其他相同作用的操作),因为被识别的类型不是唯一的。不过,注意可以添加后缀,编写代码时要考虑到这一点。

能够在优化器里(前端运行后其他某个点)识别一个特殊类型的更健壮的一个方式是,使用一个具名元数据节点来查找这个类型。例如,如果你希望查找类型%foo,你可以生成像这样的IR:

  %foo = type { ... }

  ...

  !magic.types = !{ %foo zeroinitializer }

那么要找到“foo”类型,你最好查找“magic.types”具名元数据,获取第一个元素的类型。即使类型名被剥除,或类型被自动重命名,第一个元素的类型将总是正确且稳定的。

围绕具名元数据看上去有些混淆:不像指令层面的元数据,它们不会被优化器遍丢弃或使之无效(只要它们不指向函数或其他由优化器修改的IR对象)。总体上,从前端向一个优化器或后端传递信息,具名元数据要比摆弄类型名强太多太多。

结论

总的来说,新类型系统解决了在LLVM IR长久以来存在的若干问题。若干你正在将LLVM2.x某些代码升级到3.x,很可能你会碰到一些问题。 希望这能帮助回答一些关于为什么我们进行这个改变以及它如何工作的常见问题!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值