项目中使用到了ClickHouse的bimtap结构,来分析下ClickHouse中bimtap的具体实现。
ClickHouse中bitmap结构的类型为 AggregateFunction(groupBitmap, UInt32),对应如下源码:
template
struct AggregateFunctionGroupBitmapData
{
RoaringBitmapWithSmallSet rbs;
static const char *name()
{ return "groupBitmap"; }
};
重点看下 RoaringBitmapWithSmallSet的实现:
/**
* For a small number of values - an array of fixed size "on the stack". 对于少量的值(small_set_size = 32), 在堆栈上分配固定大小的数组进行存储
* For large, roaring_bitmap_t is allocated. 对于大量的值, 使用RoaringBitmap进行存储
* For a description of the roaring_bitmap_t, see: https://github.com/RoaringBitmap/CRoaring
*/
template
class RoaringBitmapWithSmallSet : private boost::noncopyable
{
private:
using Small = SmallSet;
using ValueBuffer = std::vector;
Small small;
roaring_bitmap_t *rb = nullptr;
void toLarge()
{
rb = roaring_bitmap_create();
for (const auto &x : small)
roaring_bitmap_add(rb, x.getValue());
}
public:
bool isLarge() const
{ return rb != nullptr; }
bool isSmall() const
{ return rb == nullptr; }
......
private:
/// To read and write the DB Buffer directly, migrate code from CRoaring
//拷贝了CRoaring的代码, 用于直接读写DB Buffer
void db_roaring_bitmap_add_many(DB::ReadBuffer &dbBuf, roaring_bitmap_t *r, size_t n_args)
......
}
可以看到RoaringBitmapWithSmallSet有2个主要的成员变量:Small和roaring_bitmap_t,其中roaring_bitmap_t就是著名的RoaringBimtap(RBM)了,CK中对于大量的值(个数大于32)的存储是基于RoaringBimtap的。
对于少量数据呢,CK是基于自己实现的SmallSet
对于SmallSet
template
<
typename Key,
size_t capacity
>
using SmallSet = SmallTable, capacity>;
/** Replacement of the hash table for a small number (<10) of keys. key的数量<10的时候, 可使用SmallTable替换hash table.
* Implemented as an array with linear search. 基于线性搜索的数组实现的
* The array is located inside the object. 数组位于对象内部
* The interface is a subset of the HashTable interface. 这个接口是HashTable接口的一个子集
*
* Insert is possible only if the `full` method returns false.
* With an unknown number of different keys, you should check if the table is not full,
* and do a `fallback` in this case (for example, use a real hash table).
* 只有在full()方法返回false时才可以插入.
* 对于未知数量的不同keys, 应检查表是否未满. 如果满了, 执行“回退”, 再使用真正的哈希表
*/
template
<
typename Key,
typename Cell,
size_t capacity
>
class SmallTable :
private boost::noncopyable,
protected Cell::State
{
protected:
friend class const_iterator;
friend class iterator;
friend class Reader;
using Self = SmallTable;
using cell_type = Cell;
size_t m_size = 0; /// Amount of elements.
Cell buf[capacity]; /// A piece of memory for all elements. buf是一个Cell数组, 数组大小为capacity(capacity = 32).
/// Find a cell with the same key or an empty cell, starting from the specified position and then by the collision resolution chain.
/// 从指定位置开始, 找到一个Cell, 该Cell可能为空, 也可能包含一些keys. (如果不为空)遍历这些keys, 找到和x相同的key.
const Cell * ALWAYS_INLINE findCell(const Key & x) const
{
const Cell * it = buf;
while (it < buf + m_size)
{
if (it->keyEquals(x))
break;
++it;
}
return it;
}
Cell * ALWAYS_INLINE findCell(const Key & x)
{
Cell * it = buf;
while (it < buf + m_size)
{
if (it->keyEquals(x))
break;
++it;
}
return it;
}
public:
using key_type = Key;
using value_type = typename Cell::value_type;
......
}
可以看到SmallTable两个主要的成员变量:
size_t m_size = 0; /// Amount of elements.
Cell buf[capacity]; /// A piece of memory for all elements. buf是一个Cell数组, 数组大小为capacity(capacity = 32).
其中,buf是一个Cell数组, 数组大小为capacity(capacity = 32).
其中Cell是HashTableCell
/** hash table中cell的编译时接口
* 不同的cell类型用于实现不同的哈希表
* cell中必须包含键
* cell中还可以包含一个值和任意的附加数据(例如:存储的哈希值;ClearableHashMap的版本号)
*/
/** Compile-time interface for cell of the hash table.
* Different cell types are used to implement different hash tables.
* The cell must contain a key.
* It can also contain a value and arbitrary additional data
* (example: the stored hash value; version number for ClearableHashMap).
*/
template
struct HashTableCell
{
using State = TState;
using value_type = Key;
Key key;
HashTableCell() {}
......
}
HashTableCell中也只有一个成员变量Key。
(以上类很多都是基于C++ 类模版来实现的,暂时只写这么多)
前面提到,在针对bitmap的and/or等操作的一些方法中,也可以看到代码中会使用isSmall()或isLarge()方法去判断两个bitmap的实现,必要时使用toLarge()方法将bitmap转化成RoaringBimtap的实现,再调用RBM中的方法实现不同的操作。
下面分析几个具体的方法:
第一个,向bitmap中添加元素的时候, 根据bitmap的不同实现方式去判断.
void add(T value) //向bitmap中添加元素的时候, 根据bitmap的不同实现方式去判断.
{
if (isSmall())
{
if (small.find(value) == small.end())
{
if (!small.full())
small.insert(value);
else
{
toLarge();
roaring_bitmap_add(rb, value);
}
}
} else
roaring_bitmap_add(rb, value);
}
相对简单,一看就很清楚了。
第二个,bitmapAnd的实现
/**
* Computes the intersection between two bitmaps
* bitmapAnd的实现
*/
void rb_and(const RoaringBitmapWithSmallSet &r1)
{
ValueBuffer buffer;
if (isSmall() && r1.isSmall())
{
// intersect
for (const auto &x : small)
if (r1.small.find(x.getValue()) != r1.small.end())
buffer.push_back(x.getValue());
// Clear out the original values
small.clear();
for (const auto &value : buffer)
small.insert(value);
buffer.clear();
} else if (isSmall() && r1.isLarge())
{
for (const auto &x : small)
if (roaring_bitmap_contains(r1.rb, x.getValue()))
buffer.push_back(x.getValue());
// Clear out the original values
small.clear();
for (const auto &value : buffer)
small.insert(value);
buffer.clear();
} else
{
roaring_bitmap_t *rb1 = r1.isSmall() ? r1.getNewRbFromSmall() : r1.getRb();
roaring_bitmap_and_inplace(rb, rb1);
if (r1.isSmall())
roaring_bitmap_free(rb1);
}
}
r0 AND r1,具体过程为,判断r0的实现,如果r0.isSmall()=true,遍历r0,当前值r1中也存在则将该值放入中间结果集合buffer中,最后将buffer中的值,放回r0中;如果r0.isSmall()=false,调用RBM的roaring_bitmap_and_inplace(rb, rb1);方法进行计算。
第三个,
/**
* Computes the cardinality of the intersection between two bitmaps.
* bitmapAndCardinality的实现
*/
UInt64 rb_and_cardinality(const RoaringBitmapWithSmallSet &r1) const
{
UInt64 retSize = 0;
if (isSmall() && r1.isSmall())
{
for (const auto &x : small)
if (r1.small.find(x.getValue()) != r1.small.end())
retSize++;
} else if (isSmall() && r1.isLarge())
{
for (const auto &x : small)
if (roaring_bitmap_contains(r1.rb, x.getValue()))
retSize++;
} else
{
roaring_bitmap_t *rb1 = r1.isSmall() ? r1.getNewRbFromSmall() : r1.getRb();
retSize = roaring_bitmap_and_cardinality(rb, rb1);
if (r1.isSmall())
roaring_bitmap_free(rb1);
}
return retSize;
}
该方法是2个bitmap做完AND运算后返回结果集的基数。重点看下retSize = roaring_bitmap_and_cardinality(rb, rb1); 方法,看下RBM中的具体实现,源码如下:
uint64_t roaring_bitmap_and_cardinality(const roaring_bitmap_t *x1,
const roaring_bitmap_t *x2) {
const int length1 = x1->high_low_container.size,
length2 = x2->high_low_container.size;
uint64_t answer = 0;
int pos1 = 0, pos2 = 0;
while (pos1 < length1 && pos2 < length2) {
const uint16_t s1 = ra_get_key_at_index(&x1->high_low_container, pos1);
const uint16_t s2 = ra_get_key_at_index(&x2->high_low_container, pos2);
if (s1 == s2) {
uint8_t container_type_1, container_type_2;
void *c1 = ra_get_container_at_index(&x1->high_low_container, pos1,
&container_type_1);
void *c2 = ra_get_container_at_index(&x2->high_low_container, pos2,
&container_type_2);
answer += container_and_cardinality(c1, container_type_1, c2,
container_type_2);
++pos1;
++pos2;
} else if (s1 < s2) { // s1 < s2
pos1 = ra_advance_until(&x1->high_low_container, s2, pos1);
} else { // s1 > s2
pos2 = ra_advance_until(&x2->high_low_container, s1, pos2);
}
}
return answer;
}
这里需要了解一下RBM的high_low_container实现。
typedef struct roaring_array_s {
int32_t size;
int32_t allocation_size;
void **containers;
uint16_t *keys;
uint8_t *typecodes;
} roaring_array_t;
(摘抄两句话)RBM的主要思路是:将32位无符号整数按照高16位分桶,即最多可能有216=65536个桶,论文内称为container。存储数据时,按照数据的高16位找到container(找不到就会新建一个),再将低16位放入container中。也就是说,一个RBM就是很多container的集合。
每个32位的整形,高16位会被作为key存储到short[] keys中,低16位则被看做value,存储到Container[] values中的某个Container中。keys和values通过下标一一对应。
size则标示了当前包含的key-value pair的数量,即keys和values中有效数据的数量。
keys数组永远保持有序,方便二分查找。
container_and_cardinality()就是针对3中Container可能任意组合成的9中情况进行分别的判断。(后面就更底层了,暂时就不求甚解了)。
有兴趣的还可以看下这篇文章RoaringBitmap数据结构及原理。