学习 LLVM(20) ImutAVLTree 和 ImutAVLFactory

ImmutableSet 比较复杂,前面我们先了解了二叉查找树(binary search tree), AVL tree 才能理解其实现机制。这里先从其底层实现的 AVL 树节点,AVL 树操作工厂类开始,它们分别是 ImutAVLTree 和 ImutAVLFactory 。

写的时候使用了 MediaWiki 的语法写在 wiki 中,复制到 mediawiki 的页面中保存看,会更方便一些(并安装 syntaxhighlight 插件)。这里修改太费时间,实在是抱歉了。

ImutAVLTree

定义在文件 llvm/include/llvm/[[ADT]]/ImmutableSet.h 中。

参见:
* [[ADT]], [[ImutAVLFactory]], [[ImmutableSet]], [[ImmutableMap]]
* http://llvm.org/docs/ProgrammersManual.html#dss_immutableset

== ImutAVLTree ==
Immutable AVL-Tree:不可改变的 AVL 树的节点类。

<syntaxhighlight lang="cpp">
template <ImutInfo> class ImutAVLTree { // 注1: ImutInfo
  ImutAVLFactory *factory   // 工厂对象
  ImutAVLTree *left, *right // 左节点, 右节点
  ImutAVLTree *prev, *next  // 实现 factory 中 Cache 的 hash 冲突处理。
  uint height               // 树高度
  bool is_mutable         // 标志:是否可变。新建的节点为可变的,完成整个树之后标记为不可变的。
  bool is_digest_cached   // 标志:是否已经生成了摘要(digest),摘要一旦生成就缓存起来,在 digest 字段中。
  bool is_canonicalized   // 标志:是否已经规范化了,这里规范化应是指加入到了 factory.Cache 中了。
  T value                 // 节点保存的键和值。通过 ValInfoT trait 模板获取所需信息。
  uint32 digest           // 摘要
  uint32 ref_count        // 引用计数,计数减少到 0 的时候,会释放到 factory.freeNodes 队列(栈)中。

  key_type, value_type, Factory, iterator 等类型的定义
  friend 友类声明 ImutAVLFactory 等

  getLeft(),getRight(),getHeight(),getValue() 得到树左、右节点、高度、节点值
  getMaxElement()  // 得到最大元素
  find(),size(),begin(),end(),contains() 等容器方法
  foreach() // 中序遍历(inorder traversal)

  private this()  // 私有构造,只能从工厂类 factory 中调用
  retain(),release(),destroy() 等一些辅助方法。
}
</syntaxhighlight>

* 注1:在 ImmutableMap 中,使用了 ImutAVLTree,使用的模板参数为 ImutInfo = ImutKeyValueInfo<KeyT, ValueT>。参见 [[ImutKeyValueInfo]]。

== 实现机理 ==

=== find() ===
查找具有指定键值(key)的子树节点。如果符合条件的子树未找到,则返回 NULL。参见前面二叉查找树中 get() 方法的实现。

这是一个对二叉搜索树(binary search tree)的搜索实现:
* 1. 如果 search_key == 当前节点的 key,则找到了节点,返回。
* 2. 如果 search_key < 当前节点的 key,则查找左子树。
* 3. 否则 search_key > 当前节点的 key,则查找右子树。
* 4. 如果没有左子树、或右子树了,则返回 NULL,表示没找到。

=== foreach() ===
提供一个 functor 参数,其可通过 () 操作符调用(一般称这种 functor 为 callback),遍历这个树的每个节点/子树。遍历的顺序是 [[中序遍历]](inorder traversal)。

中序遍历顺序:先遍历左子树 left.foreach(),访问自己 callback(this),访问右子树 right.foreach()。这个函数可以容易地添加新的前序遍历和后续遍历版本的。

=== release() ===
ImutAVLTree 内部使用引用计数(refCount)来标记此节点被多少地方引用了。release() 方法的作用是引用计数-1,retain() 方法用来引用计数+1。当引用计数到达 0 的时候,表示这个节点不再被任何地方引用了,会调用 destroy() 方法。destroy() 方法中会分别释放左右子树,并请求 factory 释放此节点。

这里需要详细了解 factory 的功能才知道。参见对 [[ImutAVLFactory]] 的学习。

== 参见 ==
对ImutAVLTree 的构造等操作应该是在 factory 中进行的,参见 [[ImutAVLFactory]], [[ImmutableSet::Factory]],  [[ImutIntervalAVLFactory]]。

ImutAVLFactory

 

该类定义在文件 llvm/include/llvm/[[ADT]]/[[ImmutableSet.h]] 中。

该类为 [[ImutAVLTree]] 的工厂类。(Immutable AVL-Tree Factory class)

== ImutAVLFactory ==

<syntaxhighlight lang="cpp">
template <ImutInfoT> // 注1:ImutInfoT 缺省是 ImutContainerInfo<ValT>
class ImutAVLFactory {
  typedef, friend ImutAVLTree Tree // 类型定义,友类定义。

  // 拥有的数据。
  CacheTy cache    // 注2:CacheTy = DenseMap<uint,Tree*>
  void* Allocator  // 注3:实际类型为 BumpPtrAllocator
  vector<Tree*> createdNodes   // 已创建的节点队列。
  vector<Tree*> freeNodes      // 释放了、可用的节点队列。

  this(), this(alloc)  // 构造,如果给出 alloc,则外部拥有该 alloc。见注3
  ~this()    // 不出意外会释放 alloc,如果拥有的话。

  Tree* add(Tree *t, ValT &v)  // 向树中添加新节点,参见实现机理部分。
  Tree* remove(Tree *t, KeyT &k) // 从树中删除节点,参见实现机理部分。
  Tree* getEmptyTree()         // 得到空树。实际返回 NULL 指针作为空树。

  incrementHeight()     // 计算子树高度并+1
  getHeight(TreeTy *)   // 计算树高度。参见 ImutAVLTree::getHeight() 
}
</syntaxhighlight>

* 注1:ImutInfoT 缺省是 ImutContainerInfo<ValT>,参见 [[ImutAVLTree]]和 [[ImutContainerInfo]] 的说明。
* 注2:CacheTy 在 typedef 区定义为 [[DenseMap]]。实现机理的时候详细参考。
* 注3:Allocator 的类型实际为 [[BumpPtrAllocator]],实现者使用 LSB(该指针的最低位) 作为对该分配器的拥有标志,因此改用普通指针类 *。然后提供了 getAllocator(), ownsAllocator() 方法以访问实际指针和标志。但是这样不方便调试。
* 注4:ImutAVLTree 将 height 直接存在节点中,这样不用每次计算?。
* 实际测量 sizeof() 是 36 字节。

== 实现机理 ==
[[ImutAVLTree]] 要实现的是平衡搜索二叉树,因此在插入(add)和删除(remove)的时候,要保持树的平衡性。关于 [[AVL]] 树的定义及理论,最好参见《数据结构》类型的教材。

=== add() ===
平衡二叉树的插入(insert)语义在 ImutAVLFactory 中实现为 add() 方法,其原型为:
  Tree* add(Tree *t, ValT & v)

* 0. 实际调用 add_internal(t, v)
* 1. 创建新节点 createNode() 见下面的说明。
* 2. markImmutable(T) 标记节点为 Immutable 的。
* 3. recoverNodes() 暂时不明白什么意思。其会清空 createdNodes 队列但不知道何意?也许是回收不再使用的节点到 freeNodes 中?
* 4. getCanonicalTree() 将节点放入 Cache 中。

add_internal 实现
* if key == T.key 则值相同的节点存在,创建新节点使具有新值并返回。
* key < T.key 递归调用 add_internal(key, T.left),插入到左子树,并 balanceTree()。关于 balanceTree() 详见下面的数明。
* key > T.key 递归调用 add_internal(key, T.right),插入到右子树,并 balanceTree()


=== balanceTree() ===
在 add_internal(), remove_internal() 中使用,以平衡新创建的树。

但是我实际调试的时候,发现其并没有按照 AVL 要求的 bf>1 (bf=|hr-hl|) 的要求进行平衡。而是当 bf>2 的时候才进行平衡,可是这是红黑树的平衡方式吗?或者这样可以有效地减少旋转次数吗?

add_internal() 和 balanceTree() 结合起来有一个有趣的特性,就是当需要改变任何节点信息时,都会创建一个新的树节点,这符合 Immutable(不可改变的) 这个词的语义要求。例如下面的例子:

<syntaxhighlight lang="cpp">
  typedef ImmutableSet<int> IntSet;  // 定义一个存整数 不可变集合
  typedef ImmutableSet<int>::Factory FactT; // 定义该集合的工厂类
  FactT fac;   // 实例化一个工厂对象,后续产生集合、修改都用工厂对象。
  // 实测:sizeof(IntSet::TreeTy)=36, sizeof(IntSet)=4, sizeof(FactT)=64
 
  IntSet empty = fac.getEmptySet();  // 构造一个空集合。
  IntSet a = fac.add(empty, 7);      // 构造一个含有集合 (7)
  IntSet b = fac.add(a, 5);    // 集合为 (7 left=5)
  IntSet c = fac.add(b, 3);    // 集合为 (7 left=(5 left=3)), 注1
  IntSet d = fac.add(c, 1);    // 集合为 (5 left=(3 l=1) r=7), 注2

  d.inorder_foreach(cout << value); // 输出为 1 3 5 7
</syntaxhighlight>

* 注1:按照标准 AVL 树定义,这里要进行一次 LL 旋转,变成结构为 (5 l=3 r=7)。可实际实现未进行旋转。
* 注2:这里进行了 LL 旋转,此时 bf=2。

在上面创建 a, b, c, d 的过程中,会为每次插入新节点产生几个新的 TreeTy 节点(恰好在从根到该插入点的查找路径上)。实际最后的效果是,a、b、c、d 自己不会发生变化,所有变化都会用产生新的节点来完成,所以叫做 ImmutableSet。这种特定的语义要求,一定是和使用者那里的要求有关的。反之,我们在语法书上看到的 AVLTree 都是会在 add, remove 的时候,改变树的结构和节点字段的。

但是为实现 balanceTree() 功能,需要在树中保存和维护 height 字段,实际为了各种功能,TreeTy 的大小达到了 36 个字节(在保存 int 的数据时)。而一般 AVLTreeNode 可能只需要 16 个字节。而且 add_internal() 实现为递归调用,我们是否可以尝试不用递归的实现呢?

=== remove() ===
remove() 的实现与 add() 有相似之处,调用 remove_internal() 实际完成删除,markImmutable(), recoverNodes() 和 add() 同。

* 1. 如果要删除的 key == T.key, 则返回为 combineTrees(left, right)
* 2. key < T.key, 则递归 remove_internal(key, T.left), 然后 balanceTree()
* 3. key > T.key 则递归 remove_internal(key, T.right), 然后 balanceTree()

balanceTree() 上面已经详细说明了,下面说明 combineTrees(L, R),L,R 表示要删除的节点的左右子节点:
* 与 [[binary search tree]] 类似,如果 L,R 有一者为 null 则返回另一者;可能两者都为 null,则返回既为 null。
* 选择右子树的最左(最小)子节点,作为新的合并之后的根节点。参见前述 BST 树 delete 的说明。实际实现在 removeMinBinding() 函数中,其实现方式为递归调用。
* 合并之后的树,进行 balanceTree() 操作。

上述过程中,任何删除、平衡操作都会产生新的 TreeTy 节点,以实现 immutable 语义。因为保存了树的 height 信息,因此 balanceTree() 能够根据左右子树的 height 进行调整。同样的问题是,如果没有 height 字段而是 bf 字段,不用递归,能实现 remove() 和 balance() 吗?

=== createNode() ===
这个函数用于创建一个新的树节点,并指定其左子树、右子树、节点值。其注释似乎与代码不符合。实际代码实现中:
* 首先从 freeNodes 队列中查找是否有回收的可用节点,如果有则弹出一个可用的。
* 否则,使用 [[BumpPtrAllocator]] 分配器分配一个新节点。
* 使用 in-place new 构造节点,使其具有 left,right,value,height 等值。
* 将新创建的节点,放置到 createdNodes 队列中。

这里实际上 freeNodes 是当做"栈"的形式使用的。freeNodes 在节点析构的时候被加入进来。

=== markImmutable() ===
标记指定节点及其所有子节点为 Immutable(不可改变的) 标志。一般是新建的节点需要标记。

=== getCanonicalTree() ===
为指定的树节点(及其子节点)计算一个摘要(digest)做为 key,在 Cache 中查找,其中 Cache 是一个 DenseMap(HashMap)。树节点使用 next, prev 字段构成双向链表,以能够解决放置在 cache 产生的 digest key 冲突问题。

一个树节点放置到 Cache 中即被设置为 Canonicalized 标志。如果在 Cache 中找到了内容相同的树,则返回找到的,释放新建的那个。

你可能感兴趣的:(llvm)