文件位于 llvm/include/llvm/[[ADT]]/FoldingSet.h
fold: 折叠。
[[FoldingSet]] 对于昂贵创建或多态的对象是一个好的集合类。参见:http://llvm.org/docs/ProgrammersManual.html#dss_FoldingSet
== 定义的类或方法 ==
* FoldingSetImpl -- 实现 FoldingSet 的功能。主要结构是桶的数组。
* FoldingSetTrait -- 定义如何记录(profile)指定类型的对象。
* DefaultFoldingSetTrait -- FoldingSetTrait 的缺省实现。
* ContextualFoldingSetTrait --
* DefaultContextualFoldingSetTrait -- 上面类的缺省实现。
* FoldingSetNodeIDRef -- 描述到 FoldingSetNodeID 的引用。
* FoldingSetNodeID -- 收集一个节点的所有唯一数据位。
* FoldingSetNode
* FoldingSet
* ContextualFoldingSet
* FoldingSetIteratorImpl -- FoldingSetIterator 公共部分。
* FoldingSetIterator
* FoldingSetBucketIteratorImpl -- FoldingSetBucketIterator 公共部分。
* FoldingSetBucketIterator
* FoldingSetNodeWrapper
* FastFoldingSetNode -- FoldingSetNode 的子类,其保存 FoldingSetNodeID 信息而不是每次都调用 node 重复计算。即空间换时间。
* FoldingSetTrait<T*> -- FoldingSetTrait 对指针的特化。
== 模板类 DefaultFoldingSetTrait ==
DefaultFoldingSetTrait - 提供 FoldingSetTrait 的缺省实现。类概要:
<syntaxhighlight lang="cpp">
template<T> struct DefaultFoldingSetTrait {
Profile(T &X, FoldingSetNodeID &ID); // 要求 X 提供方法 X.Profile(ID) 完成此 Profile() 请求。
Equals(X, ID) // 测试是否 X.profile == ID。缺省实现也许不够效率,派生类可以 override 此实现。
ComputeHash(X) // 计算 X 的哈希值。派生类可以提供更优化的实现。
}
</syntaxhighlight>
也即,这个 Trait 类提供 T 的 Profile,Equals,ComputeHash 的缺省实现。为实现,缺省要求 T 提供 Profile 方法,Equals,ComputeHash 缺省使用 Profile 进行计算。
== FoldingSetTrait ==
该模板类 FoldingSetTrait<T> 从 DefaultFoldingSetTrait<T> 继承,自己没有任何额外的方法和数据。我们认为这是为了留给 T 做模板特化用的。配合 FoldingSetNodeWrapper 类,我们可以给对象添加原来并未设计,而 FoldingSet 所需的 Profile 方法。
== 模板类 DefaultContextualFoldingSetTrait ==
* contextual: 环境的,上下文的,前后有关的。
类似于 DefaultFoldingSetTrait,但提供 ContextualFoldingSetTrait 的缺省实现。
<syntaxhighlight lang="cpp">
template<T, CTXT> // CTXT: 环境,上下文
struct DefaultContextualFoldingSetTrait {
Profile(T &X, FoldingSetNodeID &ID, CTXT) // 要求 X 实现 X.Profile(ID,CTXT)
Equals(...,CTXT); // 带有 CTXT 的 Equals()
ComputeHash(...,CTXT) // 带有 CTXT 的 ComputeHash()
};
</syntaxhighlight>
这个类与 DefaultFoldingSetTrait 的区别在于多了一个 CTXT 模板参数,并在所有函数中有 CTXT 类型的参数。(但不是 CTXT& 类型的参数,奇怪吗?)
== 模板类 ContextualFoldingSetTrait ==
类似于 FoldingSetTrait,但以 DefaultContextualFoldingSetTrait 为基本实现。留给类型 T, CTXT 做特化用。
== 类 FoldingSetNodeID ==
FoldingSetNodeID - 这个类用于收集一个节点(node)的所有唯一数据位(做为唯一 Key)。当所有数据都收集到之后,为此节点产生一个 hash 值。
<syntaxhighlight lang="cpp">
class FoldingSetNodeID {
SmallVector<unsigned, 32> Bits; // 缺省使用 SmallVector 来存放唯一数据。
this() // 缺省构造,和带参数构造。参见 FoldingSetNodeIDRef
AddXXX(T) // 支持多种 T 类型的 AddXXX() 方法。包括指针,整数,字符串等。
ComputeHash() // 计算此实例的 hash 值,用于在 FoldingSet 中查找节点。
operator==() // 比较。
Intern() // 参见 FoldingSetNodeIDRef 的说明。
}
</syntaxhighlight>
当前我推测,这个类用于帮助收集指定对象 obj 的可区分于别的 obj 的数据,也即可以认为是产生 obj 的唯一 Key (Primary Key, Unique Key)。各种 AddXXX() 方法加入的数据要足以区分这个 obj。下面研究典型几个的 Add() 函数:
* AddPointer(const void *Ptr) -- Ptr 当做整数 append 到 Bits 末尾。
* AddInteger(unsigned I) -- 将 I append 到 Bits 末尾。
* AddInteger(integer) -- 类似,根据 integer 类型大小,加入的 1~2 次 unsigned 值。
* AddString() -- 先加入大小,然后加入字符串到 Bits.
* 其它的类似,总之是构建一个 vector<unsigned> Bits,用以唯一化指定的 obj。也可认为是将 obj 给 serialize() 了。。。
* ComputeHash():根据 Bits 中的所有已加入的数据,计算 hash 值。
这个对象的 sizeof() = 144 (不同机器可能有不同),一般应是引用传递。里面的 [[SmallVector]] 如果装不下了,可能会在堆中分配空间的。
== 类 FoldingSetNodeIDRef ==
FoldingSetNodeIDRef 该类引用到保留(interned)起来的 FoldingSetNodeID,底层数据保存在某个分配器分配的堆中。一般不保存 FoldingSetNodeID 自身。
<syntaxhighlight lang="cpp">
class FoldingSetNodeIDRef {
unsigned *Data; // 指向底层数组
unsigned Size; // 数组长度
this() // 构造,使用指定 Data,Size 构造。
ComputeHash() // 为 Data,Size 计算 hash 值。
operator==() // 进行 == 判断。
}
</syntaxhighlight>
* 在 FoldingSetNodeID 中的方法 Intern(BumpPtrAllocator &alloc),从 alloc 中分配空间保存 Data,然后用 Data,Size 构造 FoldingSetNodeIDRef 并返回。 alloc 负责管理分配了的内存。参见 [[BumpPtrAllocator]] 的说明。
== FoldingSetImpl ==
类 FoldingSetImpl 实现 folding set 的功能。主要的数据结构是一个桶(buckets)的数组。每个桶通过节点的 hash 值进行索引。
=== FoldingSetImpl::Node ===
这个类的定义包含在 FoldingSetImpl 里面,为方便,拿出来单独写。
该类用于保存单向链接的列表节点。
<syntaxhighlight lang="cpp">
class FoldingSetImpl::Node {
void *NextBucket; // bucket 列表中的 next 链接。
// 构造,
get|setNextInBucket() // 获取/设置链表中下一个 Node.
}
</syntaxhighlight>
在 FoldingSet 集合中的节点被要求从这个类继承,也即需要有一个单链接指针指向下一个节点。(侵入式单链表)这里没有再使用复杂的模板 Traits,特化机制等等。
=== FoldingSetImpl ===
这个类实现了 FoldingSet 的基本功能。主要的数据结构是 Buckets 数组。通过 Node 的哈希值索引,其中的节点使用 Node.next 单向链接在一起。最后一个节点的 next 指针指向 bucket,这样便于节点删除操作。
<syntaxhighlight lang="cpp">
class FoldingSetImpl {
void **Buckets; // 桶的数组。
unsigned NumBuckets; // 桶的数量,也即数组大小。
unsigned NumNodes; // 当前集合中节点的数量。数量很多时候会增长总的桶数。
this(), virtual ~() // 构造与虚析构(为什么是虚的?)。
RemoveNode(Node *) // 从集合中删除指定节点。
GetOrInsertNode(Node *) // 获得或插入节点,如果已经有了则返回,否则插入。
FindNodeOrInsertPos(ID, &InsertPos) // 查找ID的节点。不存在则返回插入点。
InsertNode(Node *) // 插入节点。
size(),empty() // 容器标准方法。
// 派生类必须实现的虚方法,和 FoldingSetTrait 类的方法一致。
virtual GetNodeProfile(), NodeEquals(), ComputeNodeHash()
}
</syntaxhighlight>
* 问题:为什么析构是虚函数?为什么 GetNodeProfile() 等也被定义为虚函数呢?
* 我们稍后在学习 FoldingSet 具体实现的时候,再看看是否需要学习几个重要函数的实现机制。
== 模板类 FoldingSet ==
FoldingSet<T> 模板类用于实现对特定的模板参数 T 实现的 FoldingSet。类 T 必须从 FoldingSetImpl::Node 类继承,并实现 Profile() 函数。
<syntaxhighlight lang="cpp">
template <T> class FoldingSet : public FoldingSetImpl {
virtual GetNodeProfile() // 实现父类的虚函数,转换节点为一个 ID
virtual NodeEquals() // 使用 Trait 类比较 Node 是否相等
virtual ComputeNodeHash() // 计算节点的哈希值
this(Log2InitSize=6) // 构造。桶的初始数量为 2 的 Log2InitSize 幂次。2**6 = 32.
iterator 实现为 FoldingSetIterator<T>, const_iterator ...
begin(),end()
bucket_iterator 实现为 FoldingSetBucketIterator<T>
bucket_begin,bucket_end()
GetOrInsertNode(Node *N) // 获取或插入节点
FindNodeOrInsertPos(ID, InsertPos) // 查找或提供插入点。
}
</syntaxhighlight>
* FoldingSet 的基本功能实现在 FoldingSetImpl 中,FoldingSet 对类型做了简单封装。为此需要看 FoldingSetImpl 的函数实现,以理解其实现机理。
== FoldingSetImpl 和 FoldingSet 实现机理 ==
为研究 FoldingSet 实现机理,我们看几个主要函数的实现,及其要点。
=== GetOrInsertNode ===
函数原型为 Node *GetOrInsertNode(Node *N),其作用是,如果已经有一个节点等于(Trait::Equals)指定的节点,则返回已有的;否则,插入 N 到集合中然后返回。
伪代码如下:
<syntaxhighlight lang="cpp">
Node *FS::GetOrInsertNode(Node *N) { // F 是 FoldingSetImpl 简写
ID = GetNodeProfile(N) // 得到节点 N 的唯一标识 ID。
Node *E, InsertPos *I = FindNodeOrInsertPos(ID) // 查找这个节点,如果没有则返回插入点
if (E) // 标识为 ID 的节点存在
return E // 则返回已经存在的这个节点
InsertNode(N, I) // 将节点 N 插入到位置 I
return N // 返回插入的节点。
}
</syntaxhighlight>
* GetNodeProfile(N) 伪代码得到节点 N 的唯一标识,结果为 FoldingSetNodeID ID
* FindNodeOrInsertPos(ID) 下面研究
* InsertNode(N,I) 下面研究
这个函数可以认为是两个子功能合并在一起:GetNode(N.ID), InsertNode(N)
=== FindNodeOrInsertPos(ID) ===
查找指定 ID 的节点,如果存在则返回;否则获得插入的位置。这个函数可以认为是两个子功能的合并:FindNode(ID), CalcInsertPos(ID)。
伪代码如下:
<syntaxhighlight lang="cpp">
Node *FS:FindNodeOrInsertPos(ID, InsertPos &I) { // I 用于做返回值。
hash = ID.ComputeHash() // 计算 ID 对应的哈希值
Bucket = GetBucketFor(hash) // 得到 ID 所在的桶(Bucket)
while (Node *n = get_next_node()) // 循环:从桶中得到下一个节点。是单链表。
if (n->ID == ID) // 如果节点 n 的 ID 与参数 ID 相同,则为找到了。
return n // 找到了,返回
// 没找到,返回空指针、插入位置。其实,就算找到了,也可以返回插入位置的。。。
InsertPos &I = Bucket
return (Node *)null
}
</syntaxhighlight>
* 注:为节省空间,桶中的节点使用了单链表链接在一起,最后一个节点指回 Bucket 自身。get_next_node() 利用指针的最低位 bit (Bit0) 置位表示指回了 Bucket。参见函数 FoldingSetImpl::GetNextPtr() 及其说明。
=== InsertNode ===
InsertNode 有两种形式,其中 InsertNode(Node*) 的调用形式转换为对 InsertNode(Node*, InsertPos I) 的调用,所以这里主要研究后一种形式。InsertPos 通过 FindNodeOrInsertPos() 方法得到。
<syntaxhighlight lang="cpp">
void FS::InsertNode(Node *N, InsertPos I) {
// 如果元素数量太多,则增大哈希表桶的总数。
Grow() if (NumNodes > 2*NumBuckets)
Bucket *= I // 插入点 I 实际上就是 Bucket 桶的指针。
N->SetNextInBucket(Next) // 设置 N 的 next 指针。具体 Next 算法见程序
*Bucket = N // 新节点插入到桶中节点列表的第一个。
}
</syntaxhighlight>
需要注意的是,Next 如果指回 Bucket 自己,则设置其指针的最后 bit 为 1。
=== RemoveNode ===
从 FoldingSet 中删除一个节点。返回 true 如果存在并删除成功;返回 false 表示不存在。
<syntaxhighlight lang="cpp">
bool FS:RemoveNode(Node *N) {
*next = N->getNextInBucket() // 得到 N 的 next 指针,如果为 null 表示不在 FS 中。
如果 next == null 表示不在 FS 中,则返回 return false
...
while () {
// 在单链表 next->next->... 中查找,直到碰到指向 N 的那个节点,
// 将其 next 指向 N 的next(修改单链表) 返回 true
// 或者发现桶的指针需要更新,则更新。返回 true
}
}
</syntaxhighlight>
这里使用了循环的单链表来寻找 next 指针及进行修改。如果不使用循环,则需要计算 N 的 ID,然后找到 Bucket,进行单链表的搜索。
综上所述,FoldingSet 上面各个函数实现了一个哈希表,解决冲突用的是链地址法。
其实,使用标准 hash_map,对 key,value 进行一些特殊处理,是不是也能解决需求呢?