LLVM笔记(16) - IR基础详解(一) underlying class
在LLVM中输入程序流以IR的形式呈现, 之前培训过如何打印与阅读文本形式的IR, 这次简要介绍一下在内存中IR的组织形式, 以及处理/转换IR时需要注意点.
本节主要介绍IR组织中最底层的数据结构(Value), 它们是如何组织的(有向图)以及如何修改它们之间的联系.
一切皆Value
当在提及Linux有一个说法是一切皆文件, 即Linux把一切设备/IO都虚拟化为文件来看待, 便于底层管理.
非常巧合的是在LLVM中也有类似的概念: 一切皆Value. Value类是LLVM中非常重要的基类, 输入程序流中的变量/常量/表达式/符号都可以被视作一个Value.
举例而言:
[03:02:36] hansy@hansy:~/source/1.llvm/llvm (master)$ clang -emit-llvm -S --target=riscv32 -O2 2.c && cat 2.ll
; ModuleID = '2.c'
source_filename = "2.c"
target datalayout = "e-m:e-p:32:32-i64:64-n32-S128"
target triple = "riscv32"
; Function Attrs: norecurse nounwind readnone
define dso_local i32 @test(i32 %a, i32 %b) local_unnamed_addr #0 {
entry:
%add = add nsw i32 %b, %a
ret i32 %add
}
attributes #0 = { norecurse nounwind readnone "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "no-frame-pointer-elim"="false" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-features"="+relax" "unsafe-fp-math"="false" "use-soft-float"="false" }
!llvm.module.flags = !{!0}
!llvm.ident = !{!1}
!0 = !{i32 1, !"wchar_size", i32 4}
!1 = !{!"clang version 9.0.0 (https://github.com/llvm-mirror/clang.git 8a55120a7d72bed6c93749e0a6dbd0a2fcd873dd) (https://github.com/llvm-mirror/llvm.git ff5f64e4c8e72159f06487684037dcd3eca2cd8e)"}
上述例子中寄存器%a, %b是Value, 运算结果%add也是Value, basic block符号entry也是Value, function符号test也是Value, !0, !1这样的metadata也是Value.
让我们先来看下Value的构成, Value类(defined in include/llvm/IR/Value.h)定义见下.
class Value {
Type *VTy;
Use *UseList;
const unsigned char SubclassID;
// ......
enum ValueTy {
#define HANDLE_VALUE(Name) Name##Val,
#include "llvm/IR/Value.def"
#define HANDLE_CONSTANT_MARKER(Marker, Constant) Marker = Constant##Val,
#include "llvm/IR/Value.def"
};
};
Value类包含多个成员, 这里先介绍最重要的三个成员VTy, UseList以及SubclassID.
- 一个值(Value)必然具有一个类型(Type), VTy用来记录这个Value的Type. 任何Value都具有一个类型, 哪怕它没有类型(void).
- LLVM引入了Use类并在Value中添加一个UseList用来跟踪并记录Value的使用者. 虽然名为UseList但只是一个Use类的指针, 之后会看到LLVM是如何关联这些对象的.
- 另外一个重要的成员是SubclassID, 这是一个const值, 用来指示这个Value的子类型. 其用于isa<>与dyn_cast<>的判断.
注意SubclassID的定义比较古怪, 对于基类类型其值定义见枚举ValueTy(该枚举由Value.def宏展开生成), 而继承类的值的定义则需见继承类中的枚举定义. 更多细节参见这里.
跟踪Value与RAUW操作
一如我们之前讨论过那样, IR中指令是顺序组织的. 然而在替换指令时我们又需要高效的更新手段, 顺序容器不满足这种需求, 所以LLVM又引入了Use类.
如果一个Value使用到了另一个Value即产生一条有向边, Use类(defined in include/llvm/IR/Use.h)被用来描述这种边.
class Use {
public:
friend class Value;
friend class User;
operator Value *() const { return Val; }
Value *get() const { return Val; }
User *getUser() const { return Parent; };
inline void set(Value *Val);
inline Value *operator=(Value *RHS);
inline const Use &operator=(const Use &RHS);
Value *operator->() { return Val; }
const Value *operator->() const { return Val; }
Use *getNext() const { return Next; }
unsigned getOperandNo() const;
static void zap(Use *Start, const Use *Stop, bool del = false);
private:
Value *Val = nullptr;
Use *Next = nullptr;
Use **Prev = nullptr;
User *Parent = nullptr;
// ......
};
Use类的设计是一个经典的双链表结构, 一共包含4个成员, 分别是:
- Val - 指向被使用的Value对象.
- Next - 下一个指向同一Value的边.
- Prev - 上一个指向同一Value的边, 注意是双向链表的设计, 这里用的二层指针.
- Parent - 使用该Value的User(边的源节点).
阅读代码时的注意点:
- Use重载了->与=操作符, 其返回的都是被使用的Value本身.
- 调用getUser()获取使用者(与第一点返回值做区分).
- Use声明了友元以供Value与User访问其成员, 因此Value/User可以调用Use的成员函数.
Use类有几个重要的成员函数, 我们之后将会遇到.
void Use::addToList(Use **List) {
Next = *List;
if (Next)
Next->Prev = &Next;
Prev = List;
*Prev = this;
}
void Use::removeFromList() {
*Prev = Next;
if (Next)
Next->Prev = Prev;
}
void Use::set(Value *V) {
if (Val) removeFromList();
Val = V;
if (V) V->addUse(*this);
}
void Value::addUse(Use &U) { U.addToList(&UseList); }
Use::addToList(Use **List)
与Use::removeFromList()
是基础的双向链表操作, 分别用来将本Use对象挂入/移出链表.
Use::set(Value *V)
是对外暴露的接口, 用来修改本Use对象所使用的Value(修改边的目的端点).
再来看下用来描述使用者的User类, 相比于Value与Use它稍稍有些复杂. 让我们思考一下三者的关系:
- 首先从抽象的角度User必须继承自Value, 因为从直观上来讲一个Value可以同时是一个Value的Usee且又是另一个Value的User.
- 另一方面Use是依赖于User存在的(只有User知道它需要多少依赖以及依赖指向哪个Value), 因此User负责对Use的管理(申请与释放).
- 由于User是一个通用的基础类, 而不同指令的操作数个数又不同, 导致Use不能作为User的成员存在(容器方式存储可以, 但是效率降低).
让我们来看下User类(defined in include/llvm/IR/User.h)的定义.
class User : public Value {
protected:
User(Type *ty, unsigned vty, Use *, unsigned NumOps)
: Value(ty, vty) {
assert(NumOps < (1u << NumUserOperandsBits) && "Too many operands");
NumUserOperands = NumOps;
// If we have hung off uses, then the operand list should initially be
// null.
assert((!HasHungOffUses || !getOperandList()) &&
"Error in initializing hung off uses for User");
}
};
可以看到User并没有扩展额外的成员, 那么Use存储在哪里呢? 奥妙在新建User对象的时候.
void *User::allocateFixedOperandUser(size_t Size, unsigned Us,
unsigned DescBytes) {
assert(Us < (1u << NumUserOperandsBits) && "Too many operands");
static_assert(sizeof(DescriptorInfo) % sizeof(void *) == 0, "Required below");
unsigned DescBytesToAllocate =
DescBytes == 0 ? 0 : (DescBytes + sizeof(DescriptorInfo));
assert(DescBytesToAllocate % sizeof(void *) == 0 &&
"We need this to satisfy alignment constraints for Uses");
uint8_t *Storage = static_cast<uint8_t *>(
::operator new(Size + sizeof(Use) * Us + DescBytesToAllocate));
Use *Start = reinterpret_cast<Use *>(Storage + DescBytesToAllocate);
Use *End = Start + Us;
User *Obj = reinterpret_cast<User*>(End);
Obj->NumUserOperands = Us;
Obj->HasHungOffUses = false;
Obj->HasDescriptor = DescBytes != 0;
for (; Start != End; Start++)
new (Start) Use(Obj);
if (DescBytes != 0) {
auto *DescInfo = reinterpret_cast<DescriptorInfo *>(Storage + DescBytes);
DescInfo->SizeInBytes = DescBytes;
}
return Obj;
}
void *User::operator new(size_t Size, unsigned Us) {
return allocateFixedOperandUser(Size, Us, 0);
}
void *User::operator new(size_t Size, unsigned Us, unsigned DescBytes) {
return allocateFixedOperandUser(Size, Us, DescBytes);
}
void *User::operator new(size_t Size) {
// Allocate space for a single Use*
void *Storage = ::operator new(Size + sizeof(Use *));
Use **HungOffOperandList = static_cast<Use **>(Storage);
User *Obj = reinterpret_cast<User *>(HungOffOperandList + 1);
Obj->NumUserOperands = 0;
Obj->HasHungOffUses = true;
Obj->HasDescriptor = false;
*HungOffOperandList = nullptr;
return Obj;
}
User重载了三个版本的new操作符, 我们先来看最下面的实现, 其在给定大小Size的基础上又增加了一个Use指针大小的空间进行内存申请. 然后将返回地址的起始作为Use指针并置空, 之后的地址作为User对象的地址初始化并返回. 即在每个User对象之前有一个Use指针大小的空间保存了一个默认为空的Use指针. 有点意思, huhhh ?
再来看下另外两个版本的实现均调用了void *User::allocateFixedOperandUser(size_t Size, unsigned Us, unsigned DescBytes)
. 有趣的是申请的内存空间是传入的Size加上Us个Use对象的空间(以及DescriptInfo的大小, 可选项, 暂时忽略). 而在初始化时又将DescriptInfo放在最前, 然后是Us个Use, 最后是User对象.
现在可以理解User是如何管理Use对象的了:
- 第一种是独立分配, 在构造User对象时额外分配一个指针用来保存Use数组, 这种情况下HasHungOffUses为true.
对于这种情况如需分配Use对象时可以调用void User::allocHungoffUses(unsigned N, bool IsPhi = false)
与void User::growHungoffUses(unsigned N, bool IsPhi = false)
. - 另一种是co-allocated, 在构造User对象时传入边的数量并分配连续内存同时保存User与Use, 这种情况下HasHungOffUses为false.
再来看下User如何访问Use对象.
class User : public Value {
private:
const Use *getHungOffOperands() const {
return *(reinterpret_cast<const Use *const *>(this) - 1);
}
Use *&getHungOffOperands() { return *(reinterpret_cast<Use **>(this) - 1); }
const Use *getIntrusiveOperands() const {
return reinterpret_cast<const Use *>(this) - NumUserOperands;
}
Use *getIntrusiveOperands() {
return reinterpret_cast<Use *>(this) - NumUserOperands;
}
public:
const Use *getOperandList() const {
return HasHungOffUses ? getHungOffOperands() : getIntrusiveOperands();
}
Use *getOperandList() {
return const_cast<Use *>(static_cast<const User *>(this)->getOperandList());
}
Value *getOperand(unsigned i) const {
assert(i < NumUserOperands && "getOperand() out of range!");
return getOperandList()[i];
}
};
在将来我们会看到Value *User::getOperand(unsigned i)
是一个非常常用的接口, 我们在编写IR变换的代码时访问IR的操作数就通过这个接口实现.
这个接口返回了一个给定下标的数组的对象, 而这个数组的地址是由Use *User::getOperandList()
返回的, 后者根据HasHungOffUses标记位决定从何处返回地址.
若HasHungOffUses为true则访问User对象之前的地址, 否则直接计算偏移.
用框图来展示一下其逻辑.
回到最初的问题: 如何高效的替换/删除Value对象? 如果仅仅简单的修改IR中的operand指针是不能起到修改/替换的作用的, 严重的会导致底层处理时错误计算def-use关系导致coredump.
因此我们必须使用LLVM提供的被称为RAUW(replace all uses with)的操作(这类技巧在LLVM代码框架中随处可见), 其接口定义见Value类.
class Value {
private:
void doRAUW(Value *New, ReplaceMetadataUses);
public:
void replaceAllUsesWith(Value *V);
};
void Value::doRAUW(Value *New, ReplaceMetadataUses ReplaceMetaUses) {
assert(New && "Value::replaceAllUsesWith(<null>) is invalid!");
assert(!contains(New, this) &&
"this->replaceAllUsesWith(expr(this)) is NOT valid!");
assert(New->getType() == getType() &&
"replaceAllUses of value with new value of different type!");
// Notify all ValueHandles (if present) that this value is going away.
if (HasValueHandle)
ValueHandleBase::ValueIsRAUWd(this, New);
if (ReplaceMetaUses == ReplaceMetadataUses::Yes && isUsedByMetadata())
ValueAsMetadata::handleRAUW(this, New);
while (!materialized_use_empty()) {
Use &U = *UseList;
// Must handle Constants specially, we cannot call replaceUsesOfWith on a
// constant because they are uniqued.
if (auto *C = dyn_cast<Constant>(U.getUser())) {
if (!isa<GlobalValue>(C)) {
C->handleOperandChange(this, New);
continue;
}
}
U.set(New);
}
if (BasicBlock *BB = dyn_cast<BasicBlock>(this))
BB->replaceSuccessorsPhiUsesWith(cast<BasicBlock>(New));
}
void Value::replaceAllUsesWith(Value *New) {
doRAUW(New, ReplaceMetadataUses::Yes);
}
注意到Value::replaceAllUsesWith(Value *New)
是一个非常重要的接口, 以后我们会看到对IR的变换都会调用这个接口. 其仅仅调用了Value::doRAUW(Value *New, ReplaceMetadataUses ReplaceMetaUses)
.
后者的实现也很简单, 除去对constant与phi节点的特殊操作(constant在IR中是unique的, 即多个constant使用同一Value, phi节点操作数中incoming block不是简单通过Use来引用的, 因此两者需要特殊处理), 就是遍历Value本身的边(Use)并将其指向的目的替换为新值.
Value跟踪与同步更新
有了RAUW支持, 我们可以通过单一接口修改IR而不用担心(底层的)正确性以及性能问题了. 然而RAUW只能保证我们当前修改的Value的及时更新, 并不能保证其它Value的有效性, 举个例子:
我现在有一个特定的优化, 它会先搜索一些Value将其指针存在一个容器里然后依次处理. 然而不幸的是这些Value之间相互关联, 可能我修改一个Value以后另一个Value就不再需要处理了, 甚至可能被删除了. 如果直接顺序处理这些Value就会导致悬空指针的问题, 那么如何更新Value指针呢?
我们肯定不能给Value添加Use, 原因是Use代表了一条边(数据依赖), 而这种情况下并不存在边. 另外很多上层操作依赖判断Value的Use是否非空, 如果加了边反而导致无法删除节点.
一个简单的想法是在处理每个Value时都去检索一遍当前处理的对象指针是否在容器中, 然而这会带来巨大的性能开销.
在这种情况下LLVM设计了另一个神兵利器: ValueHandle. 正如其字面意思, ValueHandle是存储了Value的句柄, 用于在Value被修改时同步更新. 先来看下Value是如何触发ValueHandle的.
class Value {
friend class ValueHandleBase;
unsigned char HasValueHandle : 1;
public:
bool hasValueHandle() const { return HasValueHandle; }
// ......
};
Value::~Value() {
// Notify all ValueHandles (if present) that this value is going away.
if (HasValueHandle)
ValueHandleBase::ValueIsDeleted(this);
// ......
}
Value类包含一个HasValueHandle标记, 用于指示该Value是否被ValueHandle所监视. bool Value::hasValueHandle()
一共有两处调用, 分别在析构函数与void Value::doRAUW(Value *New, ReplaceMetadataUses ReplaceMetaUses)
(见上文)中判断是否需要调用void ValueHandleBase::ValueIsDeleted(Value *V)
与void ValueHandleBase::ValueIsRAUWd(Value *V)
通知ValueHandle处理/回收.
这里的ValueHandleBase是ValueHandle的基类, 其设计理念类似于智能指针. 我们先来看下其定义(defined in include/llvm/IR/ValueHandle.h).
class ValueHandleBase {
friend class Value;
protected:
enum HandleBaseKind { Assert, Callback, Weak, WeakTracking };
ValueHandleBase(const ValueHandleBase &RHS)
: ValueHandleBase(RHS.PrevPair.getInt(), RHS) {}
ValueHandleBase(HandleBaseKind Kind, const ValueHandleBase &RHS)
: PrevPair(nullptr, Kind), Val(RHS.getValPtr()) {
if (isValid(getValPtr()))
AddToExistingUseList(RHS.getPrevPtr());
}
private:
PointerIntPair<ValueHandleBase**, 2, HandleBaseKind> PrevPair;
ValueHandleBase *Next = nullptr;
Value *Val = nullptr;
public:
explicit ValueHandleBase(HandleBaseKind Kind)
: PrevPair(nullptr, Kind) {}
ValueHandleBase(HandleBaseKind Kind, Value *V)
: PrevPair(nullptr, Kind), Val(V) {
if (isValid(getValPtr()))
AddToUseList();
}
~ValueHandleBase() {
if (isValid(getValPtr()))
RemoveFromUseList();
}
Value *operator=(Value *RHS) {
if (getValPtr() == RHS)
return RHS;
if (isValid(getValPtr()))
RemoveFromUseList();
setValPtr(RHS);
if (isValid(getValPtr()))
AddToUseList();
return RHS;
}
Value *operator=(const ValueHandleBase &RHS) {
if (getValPtr() == RHS.getValPtr())
return RHS.getValPtr();
if (isValid(getValPtr()))
RemoveFromUseList();
setValPtr(RHS.getValPtr());
if (isValid(getValPtr()))
AddToExistingUseList(RHS.getPrevPtr());
return getValPtr();
}
Value *operator->() const { return getValPtr(); }
Value &operator*() const {
Value *V = getValPtr();
assert(V && "Dereferencing deleted ValueHandle");
return *V;
}
protected:
Value *getValPtr() const { return Val; }
static bool isValid(Value *V) {
return V &&
V != DenseMapInfo<Value *>::getEmptyKey() &&
V != DenseMapInfo<Value *>::getTombstoneKey();
}
public:
// Callbacks made from Value.
static void ValueIsDeleted(Value *V);
static void ValueIsRAUWd(Value *Old, Value *New);
};
先来看下ValueHandleBase的成员, 仍然是以双向链表为组成(PrevPair的指针部分与Next分别为ValueHandleBase的前向/后向指针), Val是该ValueHandle所监视的Value指针, 注意这里的PrevPair的整数部分是这个ValueHandle的类型枚举. 当前ValueHanlde分为四类:
- Assert: 代表类型是AssertVH, 作用是当一个Value被删除时断言报错. 注意对于Value的RAUW操作该Handle并不会更新对应的值, 因此RAUW操作需要手动更新该Handle所指向的Pointer.
注意在非Debug模式下AssertVH退化为一个wrapper, 因此不会对悬空指针断言. - Callback: 代表类型是CallbackVH, 作用是在底层Value被更新或删除时调用对应的callback. CallbackVH可以被用于哈希表的Key, 前提是修改其指向的Value前必须被移出哈希表(否则pointer改变后将导致哈希表Key值不匹配).
CallbackVH自定义两个虚函数virtual void CallbackVH::deleted()
与virtual void CallbackVH::allUsesReplacedWith(Value *)
用与删除与更新时回调操作, 继承CallbackVH的类在实现这两个接口时需要遵守其注释中的限制. - Weak: 代表类型是WeakVH, 作用是在Value被删除时将自己置空.
- WeakTracking: 代表类型是WeakTrackingVH, 其类似于WeakVH, 区别是在RAUW操作时会自动更新新值. 类似于CallbackVH, 不建议将其作为哈希表的Key.
注意ValueHandleBase重载的赋值操作符, 其实现首先将自身从ValueHandleBase的链表中删除, 然后修改所指向的Value指针, 最后将自己挂入新的ValueHandleBase链表.
ValueHandleBase还定义了两个静态成员函数, 即我们在上文中看到的Value类会调用的回调ValueIsDeleted与ValueIsRAUWd. 我们来看下这两个接口的实现, 理解以上四类ValueHandle的行为.
void ValueHandleBase::ValueIsDeleted(Value *V) {
assert(V->HasValueHandle && "Should only be called if ValueHandles present");
// Get the linked list base, which is guaranteed to exist since the
// HasValueHandle flag is set.
LLVMContextImpl *pImpl = V->getContext().pImpl;
ValueHandleBase *Entry = pImpl->ValueHandles[V];
assert(Entry && "Value bit set but no entries exist");
// We use a local ValueHandleBase as an iterator so that ValueHandles can add
// and remove themselves from the list without breaking our iteration. This
// is not really an AssertingVH; we just have to give ValueHandleBase a kind.
// Note that we deliberately do not the support the case when dropping a value
// handle results in a new value handle being permanently added to the list
// (as might occur in theory for CallbackVH's): the new value handle will not
// be processed and the checking code will mete out righteous punishment if
// the handle is still present once we have finished processing all the other
// value handles (it is fine to momentarily add then remove a value handle).
for (ValueHandleBase Iterator(Assert, *Entry); Entry; Entry = Iterator.Next) {
Iterator.RemoveFromUseList();
Iterator.AddToExistingUseListAfter(Entry);
assert(Entry->Next == &Iterator && "Loop invariant broken.");
switch (Entry->getKind()) {
case Assert:
break;
case Weak:
case WeakTracking:
// WeakTracking and Weak just go to null, which unlinks them
// from the list.
Entry->operator=(nullptr);
break;
case Callback:
// Forward to the subclass's implementation.
static_cast<CallbackVH*>(Entry)->deleted();
break;
}
}
// All callbacks, weak references, and assertingVHs should be dropped by now.
if (V->HasValueHandle) {
#ifndef NDEBUG // Only in +Asserts mode...
dbgs() << "While deleting: " << *V->getType() << " %" << V->getName()
<< "\n";
if (pImpl->ValueHandles[V]->getKind() == Assert)
llvm_unreachable("An asserting value handle still pointed to this"
" value!");
#endif
llvm_unreachable("All references to V were not removed?");
}
}
void ValueHandleBase::ValueIsRAUWd(Value *Old, Value *New) {
assert(Old->HasValueHandle &&"Should only be called if ValueHandles present");
assert(Old != New && "Changing value into itself!");
assert(Old->getType() == New->getType() &&
"replaceAllUses of value with new value of different type!");
// Get the linked list base, which is guaranteed to exist since the
// HasValueHandle flag is set.
LLVMContextImpl *pImpl = Old->getContext().pImpl;
ValueHandleBase *Entry = pImpl->ValueHandles[Old];
assert(Entry && "Value bit set but no entries exist");
// We use a local ValueHandleBase as an iterator so that
// ValueHandles can add and remove themselves from the list without
// breaking our iteration. This is not really an AssertingVH; we
// just have to give ValueHandleBase some kind.
for (ValueHandleBase Iterator(Assert, *Entry); Entry; Entry = Iterator.Next) {
Iterator.RemoveFromUseList();
Iterator.AddToExistingUseListAfter(Entry);
assert(Entry->Next == &Iterator && "Loop invariant broken.");
switch (Entry->getKind()) {
case Assert:
case Weak:
// Asserting and Weak handles do not follow RAUW implicitly.
break;
case WeakTracking:
// Weak goes to the new value, which will unlink it from Old's list.
Entry->operator=(New);
break;
case Callback:
// Forward to the subclass's implementation.
static_cast<CallbackVH*>(Entry)->allUsesReplacedWith(New);
break;
}
}
#ifndef NDEBUG
// If any new weak value handles were added while processing the
// list, then complain about it now.
if (Old->HasValueHandle)
for (Entry = pImpl->ValueHandles[Old]; Entry; Entry = Entry->Next)
switch (Entry->getKind()) {
case WeakTracking:
dbgs() << "After RAUW from " << *Old->getType() << " %"
<< Old->getName() << " to " << *New->getType() << " %"
<< New->getName() << "\n";
llvm_unreachable(
"A weak tracking value handle still pointed to the old value!\n");
default:
break;
}
#endif
}
注意到两者除去最后检查部分实现几乎完全相同. ValueIsDeleted最后会检查是否还有AssertVH监视该Value, ValueIsRAUWd最后会检查是否还有WeakTrackingVH监视该Value, 这正是符合这两类ValueHandle的定义.
另外实现上注意一点是for循环中对ValueHandle的操作方式.
- 一是就像注释中解释的那样Iterator是临时构造的一个ValueHandle被用做迭代器, 其目的是在循环中修改链表而无需打断迭代器(否则需要同时记录当前指针与下一个指针).
- 二是注意到迭代中修改的是Entry, Iterator反而是不变的, 这也是为了配合第一点的目的. 在每次循环中Iterator从链表中取出, 然后加到Entry之后, 在Entry对应的ValueHandle被处理后Entry被重新设置为排在Iterator之后一个ValueHandle, 依次处理直到Entry为空.
对ValueHandle的介绍暂告段落, 再说下使用上的应用与限制.
- AssertVH用来调试告警或作为指针键值(非Debug模式退化为Value指针), 使用完毕需要手动释放句柄, 否则(Debug版本下)删除Value时会报错.
- WeakVH用来跟踪Value, 防止被跟踪对象被删除.
- WeakTrackingVH用来跟踪Value, 防止被跟踪对象失效(被删除或更新), 不适合作为哈希表键值, 对其封装的类型必须显示确保显示更新, 否则(Debug版本下)更新Value时会报错.
- CallbackVH用于在Value失效时通知应用代码处理, 不建议作为哈希表键值.
举例这里有个关于CodeGenPrep优化中报错的patch原因即代码中没有及时释放句柄导致删除Value失败.
另外一个我司代码中遇到的case同样出自CodeGenPrep(这优化bug也太多了吧), 注意到该优化中存在多个以AssertVH为键值的DenseMap, 而该优化并没有在最后释放这些句柄, 导致在这之后修改GEP指令的优化报错.
社区也有遇到此类问题, 这个patch下的回复还是很有意思的(我知道有问题, 我也看到了修改方案, 但是我觉得不好, 所以我就是不合).
设计思考
补充一些阅读代码时候的思考, 个人理解仅限参考.
- Use类的设计: 没有拷贝构造函数, 私有的析构函数, 使用替换边(Use::set())而非删除重建边的方式.
因为Use是依赖与User存在的, 一条指令定义了必然就存在固定数量的操作数, 即存在对应数量的边, 所以替换一定比删除重建更快. 同理公有的析构函数也没有意义, 因为Use一定是在User被析构时一起被析构(见void User::operator delete(void *Usr)
). - Value类的设计: 保护的构造函数与析构函数, 析构函数非虚, use迭代器设计.
类似Use, Value也不能离开继承类(指令/符号或其它)单独存在, 因此没有公有的必要. 虚函数带来虚表的开销, 对于Value这样底层的又常见的结构尽量节省空间. 如何将双向链表封装成迭代器访问(见use_iterator_impl
). - ValueHandle类的设计: 智能指针设计.