学习 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 也是用的二次探测法实现的。

相关文章
相关标签/搜索