学习 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,能否有更好的办法呢?

== 辅助模板类 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>

为了看起来易懂,我添加了 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 也是用的二次探测法实现的。

转载于:https://my.oschina.net/u/232554/blog/42364

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值