學習 LLVM(14) SmallPtrSet

文件位於 llvm/include/llvm/[[ADT]]/SmallPtrSet.h算法

文件註釋:'Normally small' pointer set -- 通常小型指針集合。數組

== 定義的類 ==
* RoundUpToPowerOfTwo -- N 取整到 2 的冪次。
* RoundUpToPowerOfTwoH -- 輔助 N 取整到 2 的冪次。
* [[SmallPtrSetImpl]]
* [[SmallPtrSetIteratorImpl]]
* [[SmallPtrSetIterator]]
* [[SmallPtrSet]]函數

== 輔助模板 RoundUpToPowerOfTwoH ==
輔助模板 RoundUpToPowerOfTwoH<N, isPowerTwo> - 若是 N 不是 2 的冪次,則增長它。這個模板類用於輔助實現 [[RoundUpToPowerOfTwo]]。性能

類概要:
<syntaxhighlight lang="cpp">
template<unsigned N, bool isPowerTwo> struct RoundUpToPowerOfTwoH {
  enum { Val = N }; // 普通版本,class::Val == N
};學習

template<unsigned N> struct RoundUpToPowerOfTwoH<N, false> {
  // 特化版本,class::Val == N,是最小的大於 N 的 2 的冪次。
  enum {
    // N|(N-1) 設置右邊的 0 比特位爲 1,如 0b00101100 -> 0b00101111。
    // We could just use NextVal = N+1, but this converges faster.  N|(N-1) sets
    // the right-most zero bits to one all at once, e.g. 0b0011000 -> 0b0011111.
    Val = RoundUpToPowerOfTwo<(N|(N-1)) + 1>::Val // 注意:配合 RoundUpToPowerOfTwo
  };
};
</syntaxhighlight>優化

若是 N 裏面 1010 中間帶 0 比較多,可能要遞歸展開幾回 RoundUpToPowerOfTwo,可否有更好的辦法呢?this

== 輔助模板類 RoundUpToPowerOfTwo ==
RoundUpToPowerOfTwo - 輔助模板類,用於求取最小的大於等於 N 的 2 的冪次。若是 N 已是 2 的冪次,則值既是 N。指針

類概要:
<syntaxhighlight lang="cpp">
template<unsigned N>
struct RoundUpToPowerOfTwo {
  static bool isPowerTwo = (N&(N-1)) == 0}; // 判斷 N 是否已是2的冪次。
  enum { Val = RoundUpToPowerOfTwoH<N, isPowerTwo>::Val }; // 取整到 2 的冪次。
}
</syntaxhighlight>orm

爲了看起來易懂,我添加了 bool isPowerTwo,其中 N&(N-1) 是典型的判斷是不是 2 的冪次的方法。對象

== 類 SmallPtrSetImpl ==
類 SmallPtrSetImpl 提供給 SmallPtrSet<> 模板類做爲公共基類。SmallPtrSet 有兩種模式,small 和 large 集合。相似於 [[SmallVector]], [[SmallString]] 等 Small 系列容器。

SmallPtrSet 裏面有在對象內的指針數組,在 small 模式下,指針加入到這個數組中。若是這個數組不夠用了,則自動增加到 large 模式。於是,當集合一般比較小的時候,應該用 SmallSet,此時不進行額外的內存分配。

large 模式下使用典型的指數探測(exponentially-probed)哈希表。空桶以非法指針值(-1)表示,這樣容許插入 null 指針。墓碑(tombstone)以另外一個非法指針值(-2)表示,以容許從集合中刪除。哈希表裝載因子達到 3/4(0.75) 的時候將自動擴充,表的大小加倍。

類概要:
<syntaxhighlight lang="cpp">
class SmallPtrSetImpl {
  PTR SmallArray[]; // 保存指針類型PTR 的數組,small 模式下用。
  PTR CurArray[];   // 當前使用的指針數組,若是 == SmallArray,則表示在 small 模式下。 
  unsigned CurArraySize; // CurArray 的大小,取值是 2 的冪次。
  unsigned NumElements;  // 已在集合中的元素數量。
  unsigned NumTombstones;// 被刪除的元素數量。
 
  this(), ~this() // 帶參數的構造、析構。protected 只能被派生類使用。
  empty(),size(),clear()  容器方法
  emptyMarker -- (void*)-1  使用 -1 做爲空槽標記。
  tombstoneMarker -- (void*)-2   使用 -2 做爲墓碑(被刪除元素)標記。
  hash(), grow(), find(), ... // 不少給派生類使用的實現輔助函數
}
</syntaxhighlight>

類中各個數據、函數的使用放在派生類 SmallPtrSet 中說明。

== 類 SmallPtrSetIteratorImpl ==
SmallPtrSetIteratorImpl - 做爲模板 SmallPtrSetIterator<> 的公共基類。

類概要以下:
<syntaxhighlight lang="cpp">
class SmallPtrSetIteratorImpl {
  void **Bucket;  // 指向 hashtable 桶的指針。爲清晰,去掉了 const 修飾符。
  this(), ==, !=, ++ 的實現,由派生類使用。
}
</syntaxhighlight>

== 類 SmallPtrSetIterator ==
爲 SmallPtrSet 實現 const_iterator,類概要以下:
<syntaxhighlight lang="cpp">
template<typename PtrTy> class SmallPtrSetIterator
  : public SmallPtrSetIteratorImpl { // 派生自...
  value_type, reference, pointer 等標準容器類型定義
 
  this(), *, ++  迭代器的實現。這是一個僅向前迭代器(forward_iterator_tag)。
}
</syntaxhighlight>

== 類 SmallPtrSet ==
模板類 SmallPtrSet 實現爲存儲少許元素而進行優化的集合。內部將對 SmallSize 參數取整到 2 的冪次。

類概要:
<syntaxhighlight lang="cpp">
template<class PtrType, unsigned SmallSize>  // 參數:指針類型,元素數量
class SmallPtrSet : public SmallPtrSetImpl {
  enum SmallSizePowTwo // 對 SmallSize 取整到 2 的冪次。用 RoundUpToPowerOfTwo 實現
  void *SmallStorage[SmallSizePowTwo+1];  // in-object 指針存儲。
 
  this()  // 普通構造與複製構造,等等。
  begin(),end(),insert(),erase(),count() 等標準容器方法實現。
  ... 其它略
}
</syntaxhighlight>

== SmallSetPtr 實現機理 ==
SmallSetPtr 的幾個主要函數實際都實如今 SmallSetPtrImpl 中,也即在 SmallSetPtr.cpp 文件中實現。主要學習 insert, erase, find 等核心的方法。

=== insert() ===
insert(ptr) -- 用於向集合中插入新元素 ptr

前述提到有兩種模式,small 和 large。插入的過程以下:
* 1. 判斷是否 small 模式?依據是 CurArray 指向的是內部 in-object 的那個 SmallStorage 數組。
* 2. 在 small 模式下,線性掃描 SmallStorage[] 以查找 ptr 是否已經存在。若是存在則返回 false 表示插入失敗(或不須要插入)
* 3. 若是沒找到,而且 SmallStorage[] 還有空間,則放到 SmallStorage[] 末尾,返回 true。
* 4. SmallStorage[] 沒有空間了,轉到 large 模式。
* 5. large 模式,判斷裝載因子(load factor) > 3/4 嗎?若是是,則 grow() 見注1.
* 6. 若是墓碑過多(空位置少於1/8),則 rehash。注2.
* 7. 爲 ptr find_bucket(),若是找到的位置上已是 ptr 了表示前面插入過了,則返回 false.
* 8. 否之該位置能夠插入,在該位置插入,更新數據並返回 true。

關於爲什麼使用 empty, tombstone 參見後面 erase(), find_bucket() 的說明。

* 注1:grow() SmallPtrSet 使用的是使用[[開放尋址法]]實現的 hash 表,所以不能有太大的裝載因子,不然致使性能急劇降低。3/4(0.75) 是一個較好的時間-空間平衡的裝載因子。
* 注2:rehash -- 刪除的元素被標記爲墓碑(tombstone),若是墓碑過多將影響搜索效率,甚至致使無限循環搜索空位置。所以當墓碑標記過多的時候,須要從新 hash() 現有元素填放到正確的位置以消除墓碑。若是 grow() 增長了新空間,則會自動進行 hash() 同時消除了墓碑,所以那種狀況不須要 rehash()。

=== erase() ===
erase(ptr) 用於從集合中刪除指定元素。

過程以下:
* 1. 若是是 small 模式,線性查找 SmallStorage[]。找到則刪除,返回true;沒找到則返回 false。
* 2. 爲 ptr find_bucket()。若是沒找到,則返回 false.
* 3. 找到了,則標記這個 bucket 位置爲墓碑(tombstone)表示元素被刪除了,返回 true。

=== find_bucket() ===
find_bucket(ptr) 爲 ptr 查找所在桶的位置,若是沒有則返回可插入的空桶的位置。

find_bucket() 僅查找 large 模式下的 CurArray,這是一個開放尋址法實現的 hash 表。
* 1. 計算 ptr 的 hash 值。這裏算法使用 hash & (桶數量-1)。由於桶數量(CurArraySize) 被要求是 2 的冪次,所以可使用 & 操做。
* 2. 循環:
* 3.  若是在 hash 的桶的就是 ptr 則找到了,返回這個桶位置。
* 4.  若是 hash 的桶是 empty,表示找到一個空位置,沒有可能有重複的 ptr 在集合中,返回一個可用的桶位置。所謂可用,指要麼用這個 empty 桶位置,要麼前面有發現一個被刪除的元素位置(tombstone),則返回被刪除位置的桶。
* 5.  若是 hash 的桶標記爲 tombstone,則表示找到一個被刪除元素的位置。和 ptr 元素衝突的可能元素依舊可能存在,須要繼續查找,但記錄下這個 tombstone 的位置,其可能在步驟 4 返回。
* 6. hash 值加上 probe_amt++,繼續探測。按照這裏的算法,應該是[[二次探測再散列]]。

墓碑(tombstone)用來標記被刪除的元素,若是沒有這種 tombstone 標記,幾個衝突的元素若是插入集合中,而前面的被刪除,則後面的就將探測不到了。

後面要學習的 DensyMap 也是用的二次探測法實現的。

相關文章
相關標籤/搜索