在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類的設計: 智能指針設計.