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.

  1. 一个值(Value)必然具有一个类型(Type), VTy用来记录这个Value的Type. 任何Value都具有一个类型, 哪怕它没有类型(void).
  2. LLVM引入了Use类并在Value中添加一个UseList用来跟踪并记录Value的使用者. 虽然名为UseList但只是一个Use类的指针, 之后会看到LLVM是如何关联这些对象的.
  3. 另外一个重要的成员是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个成员, 分别是:

  1. Val - 指向被使用的Value对象.
  2. Next - 下一个指向同一Value的边.
  3. Prev - 上一个指向同一Value的边, 注意是双向链表的设计, 这里用的二层指针.
  4. Parent - 使用该Value的User(边的源节点).

阅读代码时的注意点:

  1. Use重载了->与=操作符, 其返回的都是被使用的Value本身.
  2. 调用getUser()获取使用者(与第一点返回值做区分).
  3. 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它稍稍有些复杂. 让我们思考一下三者的关系:

  1. 首先从抽象的角度User必须继承自Value, 因为从直观上来讲一个Value可以同时是一个Value的Usee且又是另一个Value的User.
  2. 另一方面Use是依赖于User存在的(只有User知道它需要多少依赖以及依赖指向哪个Value), 因此User负责对Use的管理(申请与释放).
  3. 由于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对象的了:

  1. 第一种是独立分配, 在构造User对象时额外分配一个指针用来保存Use数组, 这种情况下HasHungOffUses为true.
    对于这种情况如需分配Use对象时可以调用void User::allocHungoffUses(unsigned N, bool IsPhi = false)void User::growHungoffUses(unsigned N, bool IsPhi = false).
  2. 另一种是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分为四类:

  1. Assert: 代表类型是AssertVH, 作用是当一个Value被删除时断言报错. 注意对于Value的RAUW操作该Handle并不会更新对应的值, 因此RAUW操作需要手动更新该Handle所指向的Pointer.
    注意在非Debug模式下AssertVH退化为一个wrapper, 因此不会对悬空指针断言.
  2. Callback: 代表类型是CallbackVH, 作用是在底层Value被更新或删除时调用对应的callback. CallbackVH可以被用于哈希表的Key, 前提是修改其指向的Value前必须被移出哈希表(否则pointer改变后将导致哈希表Key值不匹配).
    CallbackVH自定义两个虚函数virtual void CallbackVH::deleted()virtual void CallbackVH::allUsesReplacedWith(Value *)用与删除与更新时回调操作, 继承CallbackVH的类在实现这两个接口时需要遵守其注释中的限制.
  3. Weak: 代表类型是WeakVH, 作用是在Value被删除时将自己置空.
  4. 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的操作方式.

  1. 一是就像注释中解释的那样Iterator是临时构造的一个ValueHandle被用做迭代器, 其目的是在循环中修改链表而无需打断迭代器(否则需要同时记录当前指针与下一个指针).
  2. 二是注意到迭代中修改的是Entry, Iterator反而是不变的, 这也是为了配合第一点的目的. 在每次循环中Iterator从链表中取出, 然后加到Entry之后, 在Entry对应的ValueHandle被处理后Entry被重新设置为排在Iterator之后一个ValueHandle, 依次处理直到Entry为空.

对ValueHandle的介绍暂告段落, 再说下使用上的应用与限制.

  1. AssertVH用来调试告警或作为指针键值(非Debug模式退化为Value指针), 使用完毕需要手动释放句柄, 否则(Debug版本下)删除Value时会报错.
  2. WeakVH用来跟踪Value, 防止被跟踪对象被删除.
  3. WeakTrackingVH用来跟踪Value, 防止被跟踪对象失效(被删除或更新), 不适合作为哈希表键值, 对其封装的类型必须显示确保显示更新, 否则(Debug版本下)更新Value时会报错.
  4. CallbackVH用于在Value失效时通知应用代码处理, 不建议作为哈希表键值.

举例这里有个关于CodeGenPrep优化中报错的patch原因即代码中没有及时释放句柄导致删除Value失败.
另外一个我司代码中遇到的case同样出自CodeGenPrep(这优化bug也太多了吧), 注意到该优化中存在多个以AssertVH为键值的DenseMap, 而该优化并没有在最后释放这些句柄, 导致在这之后修改GEP指令的优化报错.
社区也有遇到此类问题, 这个patch下的回复还是很有意思的(我知道有问题, 我也看到了修改方案, 但是我觉得不好, 所以我就是不合).

设计思考

补充一些阅读代码时候的思考, 个人理解仅限参考.

  1. Use类的设计: 没有拷贝构造函数, 私有的析构函数, 使用替换边(Use::set())而非删除重建边的方式.
    因为Use是依赖与User存在的, 一条指令定义了必然就存在固定数量的操作数, 即存在对应数量的边, 所以替换一定比删除重建更快. 同理公有的析构函数也没有意义, 因为Use一定是在User被析构时一起被析构(见void User::operator delete(void *Usr)).
  2. Value类的设计: 保护的构造函数与析构函数, 析构函数非虚, use迭代器设计.
    类似Use, Value也不能离开继承类(指令/符号或其它)单独存在, 因此没有公有的必要. 虚函数带来虚表的开销, 对于Value这样底层的又常见的结构尽量节省空间. 如何将双向链表封装成迭代器访问(见use_iterator_impl).
  3. ValueHandle类的设计: 智能指针设计.
posted @ 2020-12-06 03:06  Five100Miles  阅读(5496)  评论(3编辑  收藏  举报