llvm中有多种分析对象,如module、function、basic block、loop、region等。前几种比较常见,在分析、优化pass中被广泛使用。而region则相对少见,但也被一些特殊的region pass所使用(如StructurizeCFG)。因此,要阅读region pass并理解其含义,需要先了解region的相关概念及使用方式。本文将介绍region的定义、llvm挖掘region并构建RegionTree的方式以及region API的一些使用。
我们先来直观地感受下LLVM中的region。编写一个test的C函数,使用clang -S -emit-llvm test.c -o test.ll
将其编译为LLVM IR,然后用opt --view-regions-only test.ll
就能得到该函数的region图:
在该图中,共有红、绿、蓝三种颜色区域,表示该函数有3个region;不同颜色区域之间可以互相包含,如蓝色区域包含绿色区域,绿色区域包含红色区域,这代表了region之间的层次关系,我们称外层区域是内层区域的父区域,内层区域是外层区域的子区域。如果把控制流图(CFG)中的所有区域按照包含关系建图,那么就能得到该CFG的RegionTree了。
那么每个颜色区域有什么特点呢?有什么方式来判断一个子图是否是一个region呢?
我们观察到每个区域的入边都只连向区域内的一个固定节点(称为entry),每个区域的出边也只连向区域外的一个固定节点(称为exit)。比如红色区域有(entry, l2)、(l6,l2)两条入边,有(l3, l5)、(l4, l5)两条出边。入边都连向l2节点,出边都连向l5节点;类似地,绿色区域入边都连向l2,出边都连向l7。于是,我们基于此观察来定义LLVM中的region。
定义1:llvm region是CFG的一个连通子图,子图的所有入边都连向Region内的entry节点,所有出边都连向Region外的exit节点。
特别地,如果region的入边与出边都只有一条的话,就称该region为simple region,否则称为extended region。其中,extended region可以通过添加merge基本块将多条入边或者出边合并成一条来转成simple region。llvm中的相关描述如下:
A simple Region is connected to the remaining graph by just two edges. One edge entering the Region and another one leaving the Region.
An extended Region (or just Region) is a subgraph that can be transform into a simple Region. The transformation is done by adding BasicBlocks that merge several entry or exit edges so that after the merge just one entry and one exit edge exists.
将extended region转化为simple region的方式如下图所示:
因为region的定义中最关键的是entry与exit节点,因此我们一般用(entry, exit)作为一个region的记号。如果region中只有一个节点,就称其为trival region。
结合region的定义,不难得到以下性质:
LLVM的RegionPass按照由内到外的顺序处理region,最外层的region最后处理。因此,LLVM需要识别出函数中的region,并将region之间的层次关系表示出后才能进行region分析。那么,最自然的方式就是构建函数的RegionTree。
我们令RegionTree中的节点表示一个region,边表示region之间的包含关系。通过对树进行逆先序遍历就能保证,一个region的所有subregions都先于该region被分析。事实上,LLVM中的RGPassManager就是按照RegionTree的逆先序遍历方式逐一处理region的。
LLVM使用RegionInfoPass实现RegionTree的构建。该pass是一个FunctionPass,需要用到节点的支配者、后支配者以及支配边界,因此该pass首先会获取函数的支配树、后支配树与支配边界,然后调用recalculate函数进行计算。相关代码如下:
bool RegionInfoPass::runOnFunction(Function &F) {
releaseMemory(); // 释放RegionInfo 占有的一些动态分配内存
auto DT = &getAnalysis<DominatorTreeWrapperPass>().getDomTree();
auto PDT = &getAnalysis<PostDominatorTreeWrapperPass>().getPostDomTree();
auto DF = &getAnalysis<DominanceFrontierWrapperPass>().getDominanceFrontier();
RI.recalculate(F, DT, PDT, DF); // 重新计算
return false;
}
recalculate函数首先创建顶级region(TopLevelRegion),并以它作为RegionTree的根。所谓顶级region,其实就是整个函数,它的entry就是函数的entry,它的exit则为空。我们可以认为函数有一个虚拟的exit,所有退出基本块(return, unreachable, abort等)都连向该虚拟exit,故而表示顶级region的exit是空的。在图1中,蓝色区域就是顶级region。创建完顶级regioin之后,recalculate会调用calculate来构建RegionTree。
calculate分两个阶段进行:
两阶段分别由函数scanForRegions、buildRegionsTree实现。
scanForRegions找region的方式暴力直接。按照支配树的后序遍历处理每个节点,对每个节点,调用findRegionsWithEntry找出所有以它为entry的regions。
template <class Tr>void RegionInfoBase<Tr>::scanForRegions(FuncT &F, BBtoBBMap *ShortCut) {
using FuncPtrT = std::add_pointer_t<FuncT>;
BlockT *entry = GraphTraits<FuncPtrT>::getEntryNode(&F);
DomTreeNodeT *N = DT->getNode(entry);
// Iterate over the dominance tree in post order to start with the small regions from the bottom of the dominance tree. If the small regions are
// detected first, detection of bigger regions is faster, as we can jump over the small regions.
for (auto DomNode : post_order(N))
findRegionsWithEntry(DomNode->getBlock(), ShortCut);
}
findRegionsWithEntry接收两个参数,第一个参数是作为entry的基本块,第二个则是辅助数据结构ShortCut,它记录每个基本块作为起始,所能形成的最大region。该函数查找region的逻辑如下:
相关代码如下:
template <class Tr>
void RegionInfoBase<Tr>::findRegionsWithEntry(BlockT *entry,
BBtoBBMap *ShortCut) {
DomTreeNodeT *N = PDT->getNode(entry); // entry所在后支配树中的节点
if (!N) return;
RegionT *lastRegion = nullptr; // 记录最近一次形成的region
BlockT *lastExit = entry; // 最近一次尝试的exit节点
while ((N = getNextPostDom(N, ShortCut))) { // 遍历后支配点
BlockT *exit = N->getBlock();
if (!exit) break; // 遍历完后支配树,退出
if (isRegion(entry, exit)) { // 判断是否是一个region,若是,则记录该region,并更新subregion
RegionT *newRegion = createRegion(entry, exit);
if (lastRegion)
newRegion->addSubRegion(lastRegion);
lastRegion = newRegion;
lastExit = exit;
}
if (!DT->dominates(entry, exit)) // entry如果不支配exit的话,无需形成更大范围的region,提前退出
break;
}
if (lastExit != entry)
insertShortCut(entry, lastExit, ShortCut); // 更新shortcut
}
使用getNextPostDom查找下一个后支配点时,会先判断N所表示的基本块是否在shortcut中,如果在,则返回postdom(ShortCut[N->getBlock),否则返回postdom(N->getBlock)。下图展示了这两种情形:
假设findRegionsWithEntry的entry参数是A,则第一次调用getNextPostDom会输入A返回B。第二次调用getNextPostDom会输入B,左图会返回postdom(ShortCut[B]),也就是l7;右图则因为不存在ShortCut[B](B无法形成region),返回postdom(B),同样是l7。
引入ShortCut可以跳过位于其他region的基本块,加速region的查找;同时也能避免形成部分相交的region,保证找到的region之间只有包含关系。
那么当我们找到了一个候选的region (entry, exit)后,怎么判断该region是一个合法的region呢?可以分两种情形进行讨论:
llvm中判断region的逻辑由isRegion实现,基本思路同上。读者可以对照着阅读源码。至此,我们已经讲解了寻找函数所有region的算法,下面将讲解如何将这些region组织成树。
在scanForRegions中,所有非trival的region都被保存在BBToRegion中。给定一个基本块BB,可以通过BBToRegion查询以它作为entry的最内层region。此外,含有相同entry的region也已经建立起了父子关系。但是目前还没有建立完整的RegionTree,同时对于region的非entry节点也还没确定其所在的region。因此buildRegionsTree需要解决这两个问题。
LLVM通过对支配树进行一次DFS遍历完成RegionTree的构建,同时将非entry节点所属的region记录到BBToRegion中。DFS函数的输入参数为当前要处理的节点N,以及一个Region R。这两个参数在DFS过程中维持了以下不变量关系:
对于情形1,不断更新R为R的父region,直至N不再是R的exit,此后的操作按照情形2或3处理;对于情形2,设置BBToRegion[N] = R;对于情形3,将X的父region设为R。
处理完后,继续DFS 支配树中N的子节点。LLVM中,DFS函数buildRegionsTree的实现及注释如下:
template <class Tr>
void RegionInfoBase<Tr>::buildRegionsTree(DomTreeNodeT *N, RegionT *region) {
BlockT *BB = N->getBlock();
// case 1:不断更新region为其父region,直至region的exit不再是BB。
while (BB == region->getExit())
region = region->getParent();
typename BBtoRegionMap::iterator it = BBtoRegion.find(BB);
// This basic block is a start block of a region. It is already in the
// BBtoRegion relation. Only the child basic blocks have to be updated.
if (it != BBtoRegion.end()) { // case 3: 将以BB为entry的最外层region的父region设置为regioin。
RegionT *newRegion = it->second;
region->addSubRegion(getTopMostParent(newRegion));
region = newRegion;
} else { // case 2: 将非entry基本块所在的region记录到BBtoRegion中
BBtoRegion[BB] = region;
}
for (DomTreeNodeBase<BlockT> *C : *N) { // 继续DFS N的子节点
buildRegionsTree(C, region);
}
}
buildRegionsTree的起始参数是函数的entry基本块与TopLevelRegion。执行完后将得到完整的RegionTree(根为TopLevelRegion)。同时,所有基本块所属的最内层region保存在BBtoRegion中。
与region关系紧密的类有RegionNode,Region以及RegionInfo 3个。
下面介绍这三个类的基本功能,具体API的使用可配合源代码的注释自行阅读。
A RegionNode represents a subregion or a BasicBlock that is part of a Region.
我们考虑一个region中的基本块,这些基本块要么属于某个subregion,要么直接归属该region。那么可以将直属于regioin的subregioin以及基本块抽象成一个节点。这就是RegionNode的设计理念。不难想到描述一个RegionNode需要以下信息:
注意到RegionNode可能代表普通基本块,因此该类不描述region的exit节点信息。如果需要遍历RegionNode的后继,可以使用RNSuccIterator迭代器,它有两种特化,对应两种后继的遍历方式:
Region继承了RegionNode类,因此自然包含了RegionNode的描述信息。但同时,也扩充了以下信息:
Region相关的API函数比较简单,可自行查阅头文件中的注释说明。这里说明一下几个容易混淆的函数以及Region内的迭代器。
RegionInfo主要用途就是实现Region的挖掘与RegionTree的构建,在之前的章节中已经讲解了这两个过程。分析的结果最终保存在两个数据结构中:
Region *TopLevelRegion
保存RegionTree的根节点,可通过它遍历整个树。
BBtoRegionMap BBtoRegion
将每个基本块映射到最小的包含它的region。