介绍
别名分析(又名指针分析,是指针分析的一种)是一类技术,试图确定两个指针是否可以指向内存中的同一对象。别名分析有许多不同的算法和分类方法:流敏感vs.流不敏感、上下文敏感vs.上下文不敏感、字段敏感vs.字段不敏感、基于统一vs.基于子集等。传统上,别名分析用Must、May或No别名response响应查询,表示两个指针总是指向同一个对象,可能指向同一个对象,或者已知永远不会指向同一个对象。
LLVM AliasAnalysis类是LLVM系统中客户使用和别名分析实现的主要接口。这个类是别名分析信息的客户端和提供该信息的实现之间的公共接口,被设计用于支持广泛的实现和客户端(但目前假定所有客户端都是流不敏感的)。除了简单的别名分析信息外,这个类还公开了Mod/Ref信息,来自那些能够提供Mod/Ref信息的实现,从而使强大的分析和转换能够很好地协同工作。
该文档包含成功实现该接口、使用它以及测试双方所必需的信息。它还解释了一些关于结果的确切含义的细节。
AliasAnalysis Class
AliasAnalysis类定义了各种别名分析实现应该支持的接口。这个类导出两个重要的枚举:AliasResult和ModRefResult,它们分别表示别名查询或mod/ref查询的结果。
AliasAnalysis接口公开了有关内存的信息,以几种不同的方式表示。具体来说,内存对象表示为起始地址和大小,函数调用表示为实际调用或执行调用的调用指令。AliasAnalysis接口还公开了一些帮助器方法,允许您为任意指令获取mod/ref信息。
所有AliasAnalysis接口都要求在涉及多个值的查询中,非常量的值都在同一个函数中定义。
AliasAnalysis.h File Reference
AliasAnalysis.h
AliasAnalysis.cpp File Reference
AliasAnalysis.cpp
表示指针
最重要的是,AliasAnalysis类提供了几个方法,用于查询两个内存对象是否别名,函数调用是否可以修改或读取内存对象,等等。对于所有这些查询,内存对象都表示为一对它们的起始地址(一个符号LLVM Value*)和一个静态大小。
将内存对象表示为起始地址和大小对于正确的别名分析非常重要。例如,考虑以下(silly,but possible)的C代码:
int i;
char C[2];
char A[10];
/* ... */
for (i = 0; i != 10; ++i) {
C[0] = A[i]; /* One byte store */
C[1] = A[9-i]; /* One byte store */
}
在本例中,basic-aa pass将消除对C[0]和C[1]的存储的歧义,因为它们访问两个不同的位置,每个位置相隔一个字节,访问每个字节。在这种情况下,Loop Invariant Code Motion (LICM) pass可以使用存储运动从循环中删除存储。相反,以下代码:
int i;
char C[2];
char A[10];
/* ... */
for (i = 0; i != 10; ++i) {
((short*)C)[0] = A[i]; /* Two byte store! */
C[1] = A[9-i]; /* One byte store */
}
在本例中,对C的两个存储相互使用别名,因为对&C[0]元素的访问是一个两个字节的访问。如果大小信息在查询中不可用,即使是第一个情况也必须保守地假定访问别名(暂时没搞明白)。
别名方法
别名方法是用于确定两个内存对象是否相互别名的主要接口。它接受两个内存对象作为输入,并根据需要返回MustAlias、PartialAlias、MayAlias或NoAlias。
与所有AliasAnalysis接口一样,alias方法需要在同一个函数中定义两个指针值,或者至少有一个值是常量。
当基于一个指针的内存引用和基于另一个指针的内存引用之间没有直接的依赖关系时,可以使用NoAlias响应。最明显的例子是两个指针指向不重叠的内存范围。另一种情况是两个指针只用于读取内存。另一个是系统处于访问指向该块内存的指针和另一个别名之间时,被释放和重新分配——在这种情况下,有一个依赖关系,但它是由释放和重新分配调节的。
一个例外是noalias关键字;“不相关的”依赖项被忽略。
当两个指针可能引用同一个对象时,就使用MayAlias响应。
当已知两个内存对象以某种方式重叠时,将使用PartialAlias响应,不管它们是否从相同的地址开始。
只有在保证两个内存对象总是从完全相同的位置开始时,才能返回MustAlias响应。MustAlias响应并不意味着指针比较相等。
getModRefInfo方法
getModReInfo方法返回关于指令执行是否可以读取或修改内存位置的信息。Mod/Ref信息总是保守的:如果一条指令可能读或写一个位置,则返回ModRef。
AliasAnalysis类还提供了一个getModRefInfo方法,用于测试函数调用之间的依赖关系。这个方法取两个调用站点(CS1 & CS2),返回NoModRef,如果两个调用都没有写到内存中;返回Ref,如果CS1读取CS2写入的内存;返回Mod,如果CS1写入内存,CS2读或写;或ModRef,如果CS1可能读或写CS2所写的内存。注意:这个关系是不可交换的。
其他有用的AliasAnalysis方法
各种别名分析实现通常会收集其他一些信息,这些信息可以被各种客户端很好地利用。
pointsToConstantMemory
当且仅当分析能够证明指针只指向不变的内存位置(函数、常量全局变量和空指针)时,pointsToConstantMemory方法返回true。这个信息可以用来精炼mod/ref信息:一个不变的内存位置是不可能被修改的。
doesNotAccessMemory and onlyReadsMemory
这些方法用于为函数调用提供非常简单的mod/ref信息。如果分析可以证明函数从未读或写内存,或者函数只从常量内存中读,则doesNotAccessMemory方法将为函数返回true。具有此属性的函数是无副作用的,并且只依赖于它们的输入参数,如果它们形成公共子表达式或被悬挂出循环,则允许它们被消除。许多常见函数都是这样的(例如,sin和cos),但许多其他函数不是这样的(例如,acos,它修改errno变量)。
如果分析可以证明(最多)函数只从非易失性内存中读取,则onlyReadsMemory方法将为函数返回true。具有此属性的函数是无副作用的,仅取决于它们的输入参数和调用时的内存状态。这个属性允许消除和移动对这些函数的调用,只要没有改变内存内容的存储指令。注意,所有满足doesNotAccessMemory方法的函数满足onlyReadsmemory。
getMustAliases(Value P, std::vector
获取一定与指针P存在别名关系的变量列表。
重写一个新的AliasAnalysis实现
为LLVM编写一个新的别名分析实现非常简单。已经有一些实现可以用作示例,下面的信息应该有助于填充任何细节。举个例子,看看LLVM中包含的各种别名分析实现(↓)。
不同的pass风格
确定需要为别名分析使用何种类型的LLVM通道的第一步。与大多数其他分析和转换的情况一样,答案应该是相当明显的,从试图解决的问题类型:
①如果需要过程间分析,它应该是Pass。
②如果是一个函数局部分析,选FunctionPass。
③如果根本不需要查看程序,选ImmutablePass。
当然,除了子类的传递之外,您还应该从AliasAnalysis接口继承,并使用RegisterAnalysisGroup模板注册为AliasAnalysis的实现。
需要初始化的调用
AliasAnalysis的子类需要调用AliasAnalysis基类上的两个方法:getAnalysisUsage和InitializeAliasAnalysis。特别是,你的getAnalysisUsage的实现应该显式调用AliasAnalysis::getAnalysisUsage方法,除了声明任何你的pass依赖的pass。因此你应该有这样的东西:
void getAnalysisUsage(AnalysisUsage &AU) const {
AliasAnalysis::getAnalysisUsage(AU);
// declare your dependencies here.
}
此外,你必须在你的分析运行方法中调用InitializeAliasAnalysis方法(Pass中实现的run()方法, FunctionPass中实现的runOnFunction()方法,或ImmutablePass中实现的InitializePass()方法)。
例如(作为pass的一部分):
bool run(Module &M) {
InitializeAliasAnalysis(this);
// Perform analysis here...
return false;
}
需要重写一些方法
你必须覆盖AliasAnalysis的所有子类的getAdjustedAnalysisPointer方法。这个方法的实现示例如下:
void *getAdjustedAnalysisPointer(const void* ID) override {
if (ID == &AliasAnalysis::ID)
return (AliasAnalysis*)this;
return this;
}
可能要指定的接口
所有AliasAnalysis虚拟方法默认提供链接到另一个别名分析实现,最终返回保守正确的信息(分别为别名和Mod/Ref查询返回“May”alias和“Mod/Ref”)。
AliasAnalysis链接行为
每个别名分析pass将链到另一个别名分析实现(例如,用户可以指定“-basic-aa -ds-aa -licm”以从两个别名分析中获得最大收益)。对于您没有覆盖的方法,别名分析类会自动处理其中的大部分。对于覆盖的方法,在返回保守MayAlias或Mod/Ref结果的代码路径中,只需返回超类计算的结果。例如:
AliasResult alias(const Value *V1, unsigned V1Size,
const Value *V2, unsigned V2Size) {
if (...)
return NoAlias;
...
// Couldn't determine a must or no-alias result.
return AliasAnalysis::alias(V1, V1Size, V2, V2Size);
}
除了分析查询之外,如果覆盖了LLVM更新通知方法,还必须确保无条件地将LLVM更新通知方法传递给超类,这允许更新更改中的所有别名分析(不明白)。
更新转换的分析结果
别名分析信息最初是为程序的静态快照计算的,但客户将使用该信息对代码进行转换。除了最琐碎的别名分析形式外,所有形式的别名分析都需要更新其分析结果,以反映这些转换所做的更改。
AliasAnalysis接口公开了四种方法,它们用于从客户端向分析实现传递程序更改。各种别名分析实现应该使用这些方法来确保它们的内部数据结构在程序更改时保持最新(例如,当一条指令被删除时),并且别名分析的客户端必须确保适当地调用这些接口。
deleteValue方法
当转换从程序中删除一条指令或任何其他值(包括不使用指针的值)时,将调用deleteValue方法。别名分析通常保存程序中每个值都有条目的数据结构。当调用此方法时,它们应该删除指定值的任何项(如果它们存在的话)。
copyValue方法
copyValue方法在程序中引入新值时使用。没有办法向程序中引入以前不存在的值(这对安全的编译器转换没有意义),因此这是引入新值的唯一方法。此方法指示新值与要复制的值具有完全相同的属性。
replaceWithNewValue方法
此方法是一个简单的帮助器方法,提供此方法是为了使客户机更容易使用。它通过将旧的分析信息复制到新值,然后删除旧值来实现。别名分析实现不能覆盖此方法。
addEscapingUse方法
addEscapingUse方法用于指针值的使用发生变化,可能使预计算的分析信息无效的情况。实现可以使用此回调为自分析时间以来使用发生变化的点提供保守响应,也可以重新计算它们的部分或全部内部状态,以继续提供准确的响应。
一般来说,指针值的任何新使用都被认为是转义使用,必须通过回调报告,除了下面的使用:
指针的bitcast或getelementptr
通过指针的存储(但不是指针的存储)
通过指针加载
效率问题
从LLVM的角度来看,要提供有效的别名分析,惟一需要做的事情就是确保别名分析查询得到快速响应。别名分析结果的实际计算(“run”方法)只执行一次,但是可能执行许多(可能重复的)查询。因此,尽量将计算移到run方法中(在合理范围内)。
限制
AliasAnalysis基础结构有几个限制,这使得编写新的AliasAnalysis实现变得困难。
没有办法覆盖默认别名分析。这将是非常有用的,能够做一些像“opt -my-aa -O2”,并让它使用-my-aa为所有需要AliasAnalysis的pass,但目前没有支持,缺少更改源代码和重新编译。同样,也没有办法将一个分析链设置为默认值。
转换pass没有办法声明它们保留AliasAnalysis实现。AliasAnalysis接口包括deleteValue和copyValue方法,目的是允许一个pass保持AliasAnalysis的一致性,但是一个pass没有办法在它的getAnalysisUsage中声明它这样做了。有些传球尝试使用AU.addPreserved,然而这实际上没有任何影响。
类似地,opt -p选项在每个pass之间引入ModulePass pass,这阻止了FunctionPass别名分析pass的使用。
AliasAnalysis API有在值被删除或复制时通知实现的函数,但是这些函数还不够。还有许多其他方法可以修改LLVM IR,这些方法可能与无法表达的AliasAnalysis实现相关。
AliasAnalysisDebugger实用工具似乎建议AliasAnalysis实现可以预期在出现在别名查询中之前,它们将被告知任何相关的Value。然而,流行的客户端(如GVN)不支持此功能,并且已知在使用AliasAnalysisDebugger运行时会触发错误。
AliasSetTracker类(由LICM使用)产生了不确定数量的别名查询。这可能会导致在预定数量的查询之后暂停执行的调试技术不可靠。
许多别名查询可以用其他别名查询重新表示。当多个AliasAnalysis查询链接在一起时,从链的开始开始这些查询是有意义的,要小心避免无限循环,但是目前希望这样做的实现只能从自己开始这样的查询。
使用别名分析结果
有几种不同的方法来使用别名分析结果。按照优先顺序,这些是:
使用MemoryDependenceAnalysis Pass
memdep pass使用别名分析来提供关于内存使用指令的高级依赖信息。例如,这将告诉您将哪个提要存储到负载中。它使用缓存和其他技术来提高效率,并被Dead Store Elimination、GVN和memcpy优化所使用。
使用AliasSetTracker class
许多转换需要有关在某些作用域中活动的别名集的信息,而不是有关成对别名的信息。AliasSetTracker类用于从AliasAnalysis接口提供的成对别名分析信息有效地构建这些别名集。
首先,通过使用“add”方法来初始化AliasSetTracker,以在您感兴趣的范围内添加关于各种可能的别名化指令的信息。一旦所有别名集都完成了,pass应该使用AliasSetTracker begin()/end()方法简单地遍历所构造的别名集。
由AliasSetTracker形成的AliasSets保证是不相交的,计算集合的mod/ref信息和波动性,并跟踪集合中的所有指针是否都是Must别名。AliasSetTracker还确保集合由于调用指令而被正确折叠,并且可以在每个集合中提供一个指针列表。
作为一个示例用户,Loop Invariant Code Motion pass使用aliasSetTracker为每个循环嵌套计算别名集。如果没有修改循环中的AliasSet,那么来自该集合的所有加载指令都可能被悬挂出循环。如果任何别名集被存储到并且必须是别名集,那么存储可能会沉到循环外部,在循环嵌套期间将内存位置提升到寄存器。这两种转换仅在指针参数是循环不变的情况下才适用(没看懂)。
AliasSetTracker实现
AliasSetTracker类的实现是尽可能高效的。当一个指针插入到AliasSetTracker(别名多个集合)时,它使用union-find算法有效地合并AliasSets。主要的数据结构是一个哈希表,将指针映射到它们所在的AliasSet。
AliasSetTracker类必须维护每个AliasSet中所有LLVM值的列表。由于哈希表中已经有每个感兴趣的LLVM Value*的条目,AliasesSets通过这些哈希表节点线程链表,以避免不必要的分配内存,并使合并别名集非常高效(链表合并是常数时间)。
直接使用AliasAnalysis接口
如果这两个实用工具类都不是pass需要的,那么应该直接使用AliasAnalysis类公开的接口。尽可能使用更高级别的方法(例如,如果可能,使用mod/ref信息而不是直接使用别名方法)以获得最好的精度和效率。
Available AliasAnalysis implementations
-basic-aa pass
-basic-aa pass是一种激进的本地分析,了解许多重要事实:
·不同的全局变量、堆栈分配和堆分配永远不能别名。
·全局、堆栈分配和堆分配永远不会给空指针加上别名。
·结构的不同字段不能别名。
·具有静态下标不同的数组的索引不能别名。
·许多通用的标准C库函数从不访问内存或只读取内存。
·显然指向全局常量的指针“pointToConstantMemory”。
·函数调用不能修改或引用堆栈分配,如果它们从未逃离分配它们的函数(自动数组的常见情况)。
The -globalsmodref-aa pass
这个pass实现了一个简单的对(不要“把他们的地址记占用”的???)内部全局变量进行上下文敏感的mod/ref和别名分析。如果全局变量的地址没有被获取,则pass知道没有指针作为全局变量的别名。这pass还跟踪它知道的从不访问内存或从不读取内存的函数。这允许某些优化(例如GVN)完全消除调用指令。
这个pass的真正威力在于它为调用指令提供了上下文敏感的mod/ref信息。这允许优化器知道对函数的调用不会破坏或读取全局变量的值,从而允许消除加载和存储。
注意:此传递在其作用域上有一定的限制(仅支持非地址取全局值),但分析非常快速。
The -steens-aa pass
-steens-aa pass实现了著名的“Steensgaard算法”的一个变体,用于过程间别名分析。Steensgaard的算法是一种基于统一、流不敏感、上下文不敏感和字段不敏感的别名分析,并且具有很强的可扩展性(有效的线性时间)。
LLVM -steens-aa通过使用数据结构分析框架实现了Steensgaard算法的“推测字段敏感”版本。这使得它在保持良好的分析可伸缩性的同时,大大高于标准算法。
注意:-steens-aa可在可选的" poolalloc “模块中使用。它不是LLVM核心的一部分。
The -ds-aa pass
-ds-aa pass实现了完整的数据结构分析算法。数据结构分析是一种基于模块的统一的、流不敏感的、上下文敏感的、speculatively field-sensitive的别名分析,它也是相当可扩展的,通常在O(n * log(n))。
该算法能够响应各种别名分析查询,并可以提供上下文敏感的mod/ref信息。到目前为止,唯一没有实现的主要功能是对必须别名信息的支持。
注意:-ds-aa可在可选的” poolalloc "模块中使用。它不是LLVM核心的一部分。
The -scev-aa pass
通过将AliasAnalysis查询转换为ScalarEvolution查询来实现AliasAnalysis查询。
这使它比其他别名分析更全面地理解getelementptr指令和循环诱导变量。
Alias analysis driven transformations
LLVM包括几个别名分析驱动的转换,可以与上面的任何实现一起使用。
The -adce pass
adce pass,它实现了积极的死亡代码消除,使用AliasAnalysis接口来删除对没有副作用的函数的调用。
The -licm pass
licm通道实现了各种与循环不变代码运动相关的转换。它使用AliasAnalysis接口进行几个不同的转换:
·如果循环中没有修改已加载内存的指令,它使用mod/ref信息将加载指令提升或sink出循环。
·它使用mod/ref信息将不写入内存且循环不变的函数调用提升出循环。
·它使用别名信息将加载和存储到循环中的内存对象提升到寄存器中。
·如果加载/存储的内存位置没有可能别名,它可以这样做。
The -argpromotion pass
argpromotion pass的是按引用传递参数,而不是按值传递参数。特别是,如果指针参数只从它加载,则将加载的值而不是地址传递给函数。此传递使用别名信息,以确保从参数指针加载的值不会在函数的入口和指针的任何加载之间被修改。
The -gvn, -memcpyopt, and -dse passes
这些pass使用AliasAnalysis信息来推断负载和存储。
用于调试和评估实现的Cliens
这些pass对于评估各种别名分析实现非常有用。你可以用以下命令来使用它们:
The -print-alias-sets pass
-print-alias-sets pass作为opt工具的一部分被公开,用于打印由AliasSetTracker类形成的Alias Sets。可以这样使用:
opt -ds-aa -print-alias-sets -disable-output
The -aa-eval pass
-aa-eval pass简单地遍历函数中的所有指针对,并询问别名分析指针是否为别名。这表明了别名分析的精度。统计信息将显示找到no/may/must别名的百分比(更精确的算法可能有更少的别名)。
Memory Dependence Analysis
注意:我们目前正在进行从MemoryDependenceAnalysis到MemorySSA的迁移。
请试着用那个代替。
MemDep是位于别名分析之上的一种懒惰的缓存层,它能够在块内或块间级别上回答给定指令依赖于哪些前面的内存操作的问题。由于MemDep的惰性和缓存策略,与直接访问别名分析相比,使用MemDep可以获得显著的性能优势。