學習 LLVM(20) ImutAVLTree 和 ImutAVLFactory

ImmutableSet 比較複雜,前面咱們先了解了二叉查找樹(binary search tree), AVL tree 才能理解其實現機制。這裏先從其底層實現的 AVL 樹節點,AVL 樹操做工廠類開始,它們分別是 ImutAVLTree 和 ImutAVLFactory 。html

寫的時候使用了 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 隊列(棧)中。ui

  key_type, value_type, Factory, iterator 等類型的定義
  friend 友類聲明 ImutAVLFactory 等this

  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 中找到了內容相同的樹,則返回找到的,釋放新建的那個。

相關文章
相關標籤/搜索