高效通用bitmap

真是无语了。。。昨天写了一半了,结果不知啥原因,就变成乱码了,只好重写。

Bitmap是一个非常通用的基础设施,通常用来表示资源的可用状态。Bitmap可以使用一个字节数组来简单实现,逻辑上的第i个bit对应字节数组中第i/8个字节的第i%8个位。这种实现方式呢,随机访问(查询或者设置某个bit的状态)非常高效,可以在O(1)的时间复杂度内完成。但是搜索(比如查找一个值为1的位)的效率很差,因为顺序遍历需要O(n)的时间复杂度。但是,bitmap最最常用的功能就是搜索了,比如需要分配一个空闲的资源。我的很多项目都因为上述的这种简单bitmap而性能低下。所以我迫切需要一个高性能(至少对搜索操作高性能)的bitmap实现。

按照经验,把一个O(n)的遍历搜索加速的办法通常是使用二分搜索,变成O(logn)。那么问题是,一个很长的字节数组,折半之后,搜索不是依然需要顺序遍历吗?而且n/2 + n/4 + n/8 + ... + 1 趋近与n,根本不可能节省时间。因此,肯定不能使用字节数组这么简单的数据结构。

================阶段一:巧妙的bmp64=============

既然通用的bitmap没有头绪,那么我就退而求其次呗~我先考虑一个长度为64的bitmap。

我想到了一个好办法,利用了这么一个原理:在64位机器上,判断一个uint64_t是否为0的时间开销是固定的。如果我用一个uint64_t表示一个长度为64的bitmap,那么判断这64bit中是否有1,只需要判断这个uint64_t是否为0。类似的,比如我要判断第4位到第7位中是否有1,只需要把这个bitmap“与”0xf0,再判断是否是0即可。这样,搜索区域先从64bit变成32bit,再变成16bit,每次折半后,搜索的开销都是固定的指令数量,而不需要挨个遍历各个位。

举个例子,比如我有一个长度为64的bitmap

0000 0010 1000 0000 1100 0000 0000 1111 0000 0000 0000 0000 0000 0000 0000 0000

我用一个uint64_t来表示它:

uint64_t bitmap = 0x0280900f00000000;

首先判断bitmap != 0,那么就知道这64个bit中肯定有1。

二分搜索嘛,肯定少不了两个索引left和right,来表示搜索区域。虽然习惯上left < right,但是这里不同,规定left > right。这是因为,习惯上,我们表示一串二进制时,会把高位写在左边,把低位写在右边,就像上面的那串。起始时:

size_t left = 63;
size_t right = 0;

表示left和right中的所有bit,包括left和right所在位置的bit,是搜索区域。接下来就开始正式搜索了。

size_t mid = (left + right) / 2; // mid = 31
uint64_t mask = ((uint64_t)1 << (mid + 1)) - ((uint64_t)1 << right); // mask = 0xffffffff

这个mask是怎么回事呢?我们已经得知(64: 0]这64bit中一定有1,那么我们分成(64: 32]和(32: 0]两段,并且考察低位的(32 :0]这段里有没有1。考察(a: b]这么一段,就需要把其他的位都屏蔽掉,那么mask的第0~(b-1)位是0,第b~a位是1,第(a+1)~63位是0。

((uint64_t)1 << (a + 1)) - 1

能够产生低a位都是1、其余位都是0的掩码。

((uint64_t)1 << b) - 1

能够产生低(b-1)位都是1、其余位都是0的掩码。那么两者相减,就能产生所要的掩码。这就是上面mask的公式的原理。

if(bitmap & mask)
    left = mid;
else
    right = mid + 1;

上面代码中,我们先考察[mid: right]这一段是否有1,如果有,那么就让left = mid,也就是接下来在低位的半段搜索了,否则就在高位的半段搜索。为啥是right = mid + 1呢?因为既然[mid: right]中没有1,那么第mid位也肯定没有1,所以有1的肯定从mid + 1开始。于是,二分迭代就建立起来了!

bmp64.h

#ifndef BMP64_H
#define BMP64_H

#include <stdint.h>
#include <stdlib.h>

// 一个最长64位的bitmap
struct bmp64
{
    // 长度
    size_t len;
    // 最多64个位
    uint64_t bits;
};

#define BMP64_NOT_FOUND ((size_t)(-1))

// 初始化一个bmp64
//   len: 长度,1~64
//   value_all: 把所有位初始化为0还是1
// 成功返回1, 失败返回0
int bmp64_init(struct bmp64* bmp64, size_t len, int value_all);

// 查询指定位的值
//   bmp64: 一个bmp64的指针
//   index: 位的编号
// 失败返回-1,成功返回1或0
int bmp64_get(struct bmp64* bmp64, size_t index);

// 设置指定位的值
//   bmp64: 一个bmp64的指针
//   index: 位的编号
//   value: 位的值
// 成功返回1, 失败返回0
int bmp64_set(struct bmp64* bmp64, size_t index, int value);

// 判断bmp64中,是否存在1或0
//   bmp64: 一个bmp64的指针
//   value: 位的值
// 存在返回1,不存在返回0
int bmp64_exist(struct bmp64* bmp64, int value);

// 在bmp64中,搜索一个值为value的位
//   bmp64: 一个bmp64的指针
//   value: 位的值
// 存在则返回找到的位的编号,不存在则返回BMP64_NOT_FOUND
size_t bmp64_search(struct bmp64* bmp64, int value);

#endif

bmp64.c

#include <bmp64.h>
#include <common.h>

int bmp64_init(struct bmp64* bmp64, size_t len, int value_all)
{
    assert(bmp64);
    if(len == 0 || len > 64)
        ERROR(0, 0, "param <len = %lu> is invalid!", len);
    bmp64->len = len;
    bmp64->bits = value_all ? (~((uint64_t)0)) : 0;
    return 1;
}

int bmp64_get(struct bmp64* bmp64, size_t index)
{
    assert(bmp64);
    if(index >= bmp64->len)
        ERROR(-1 , 0, "param <index = %lu> is bigger than <bmp64->len = %lu>!", index, bmp64->len);
    assert(index < 64);
    return (bmp64->bits >> index) & (uint64_t)1;
}

int bmp64_set(struct bmp64* bmp64, size_t index, int value)
{
    assert(bmp64);
    if(index >= bmp64->len)
        ERROR(-1 , 0, "param <index = %lu> is bigger than <bmp64->len = %lu>!", index, bmp64->len);
    assert(index < 64);
    if(value)
        bmp64->bits |= ((uint64_t)1 << index);
    else
        bmp64->bits &= ~((uint64_t)1 << index);
    return 1;
}

int bmp64_exist(struct bmp64* bmp64, int value)
{
    assert(bmp64);
    assert(bmp64->len <= 64);
    uint64_t mask = bmp64->len == 64 ? (uint64_t)(-1) : ((uint64_t)1 << bmp64->len) - 1;
    if(value)
        return (bmp64->bits & mask) != 0;
    else
        return (bmp64->bits | ~mask) != (uint64_t)(-1);
}

size_t bmp64_search(struct bmp64* bmp64, int value)
{
    assert(bmp64);
    if(!bmp64_exist(bmp64, value))
        return BMP64_NOT_FOUND;
    if(value)
    {
        size_t left = bmp64->len - 1, right = 0;
        while(left > right)
        {
            size_t mid = (left + right) / 2;
            assert(mid < 63);
            uint64_t mask = ((uint64_t)1 << (mid + 1)) - ((uint64_t)1 << right);
            if(bmp64->bits & mask)
                left = mid;
            else
                right = mid + 1;
        }
        size_t index = left;
        return (bmp64->bits >> index) & (uint64_t)1 ? index : BMP64_NOT_FOUND;
    }
    else
    {
        struct bmp64 reverse;
        reverse.len = bmp64->len;
        reverse.bits = ~(bmp64->bits);
        return bmp64_search(&reverse, 1);
    }
}

其中用到的common.h与《伙伴分配器(buddy allocator)》中的一样。

=================阶段二:嵌套的bitmap=================

有了bmp64,那么最大64位长度的bitmap就完成了。可是很多时候64位不够啊,怎么办?能否基于bmp64构造任意长度的bitmap?

我初步想到了一个方案:使用n(0 < n <= 64)个bmp64,来表示最大长度64*n的bitmap。并且在上层,再用两个bitmap,来表示下层的第i个bitmap中是否有0或有1。拓扑结构如下:

这是一个双层结构,上面是两个bmp64,分别名为bmp64_has_0和bmp64_has_1。下层是64个bmp64,取名为sub[0], sub[1] ... sub[63]。bmp64_has_0的第i位如果为1,则表明sub[i]中存在0,否则不存在0。同理,bmp64_has_1的第i位如果为1,则表明sub[i]中存在1,否则不存在1。

因为sub[0]是0xffffffffffffffff,只有1没有0,所以bmp64_has_0的第0位是0,而bmp64_has_1的第0位是1。因为sub[1]是0x0123456789ABCDEF,既有0又有1,所以bmp64_has_0的第0位是1,而bmp64_has_1的第0位也是1。因为sub[63]是0x0000000000000000,只有0没有1,所以bmp64_has_0的第0位是1,而bmp64_has_1的第0位是0。

要访问逻辑上的第i位,其实就是对sub[i / 64]的第 i % 64位进行访问。接下来就是搜索问题。比如要搜索一个0,那么就在bmp64_has_0中搜索到一个1,比如返回编号是1,表明sub[1]中存在0。那么接着就在sub[1]中搜索0,比如返回编号是4,那么就得知,逻辑上的第1 * 64 + 4 = 68位是一个0。

对于其进行set操作,就需要修改bmp64_has_0和bmp64_has_1以保持一致性。当把第i位设置为0的时候,那么bmp64_has_0的第i / 64位直接设为1即可。而对于bmp64_has_1的第i / 64位,则为bmp64_exist(sub[i / 64], 1)的返回结果。对于设置1也是类似的。

当然,也可以叠加三层,四层,这样bitmap的长度就可以任意长。

Talk is cheap, show you my code!

bitmap.h

#ifndef BITMAP_H
#define BITMAP_H

#include <stdlib.h>

// 一个bitmap
struct bitmap
{
    // 长度
    size_t len;
    // 一个数组,每个元素是一个struct bitmap或者struct bmp64
    void* what;
};

#define BITMAP_NOT_FOUND ((size_t)(-1))

// 初始化bitmap
//    len: 长度,任意长
//    value_all: 把所有bit初始化为0或1
// 成功返回1,失败返回0
int bitmap_init(struct bitmap* bitmap, size_t len, int value_all);

// 查询指定位的值
//   bitmap: 一个bitmap的指针
//   index: 位的编号
// 失败返回-1,成功返回1或0
int bitmap_get(struct bitmap* bitmap, size_t index);

// 设置指定位的值
//   bitmap: 一个bitmap的指针
//   index: 位的编号
//   value: 位的值
// 成功返回1, 失败返回0
int bitmap_set(struct bitmap* bitmap, size_t index, int value);

// 判断bitmap中,是否存在1或0
//   bitmap: 一个bitmap的指针
//   value: 位的值
// 存在返回1,不存在返回0
int bitmap_exist(struct bitmap* bitmap, int value);

// 在bitmap中,搜索一个值为value的位
//   bitmap: 一个bitmap的指针
//   value: 位的值
// 存在则返回找到的位的编号,不存在则返回BITMAP_NOT_FOUND
size_t bitmap_search(struct bitmap* bitmap, int value);

#endif

这里有个奇迹淫巧需要解释一下,就是这个struct bitmap结构。从上面那种图片中可以看到,最底层是若干个struct bmp64。所以应该是:

// 两层结构
struct bitmap
{
    size_t len;
    struct bmp64 bmp64_has_0;
    struct bmp64 bmp64_has_1;
    struct bmp64* subs;
};

但是,如果位数超过4096呢?那么就需要三层结构,那么第二层也是struct bitmap,而不是struct bmp64,也就是这样:

// 多层(三层及以上)结构
struct bitmap
{
    size_t len;
    struct bmp64 bmp64_has_0;
    struct bmp64 bmp64_has_1;
    struct bitmap* sub;
};

从而让struct bitmap层层嵌套。而如果位数在64位以内,那么struct bitmap就应该退化为struct bmp64,也就是

// 单层结构,退化为bmp64
struct bitmap // struct bmp64
{
    size_t len;
    uint64_t bits;
};

可见,除了len字段以外,其他字段都应该可以伸缩。所以我设计了这么一个结构:

struct bitmap
{
    // 长度
    size_t len;
    // 一个数组,每个元素是一个struct bitmap或者struct bmp64
    void* what;
};

确保sizeof(struct bitmap) == sizeof(struct bmp64)。同时bitmap中的len的偏移量和bmp64中的偏移量相同。对于what字段的解释根据len的不同而不同:

  1. 当0 < len <= 64时,void* what解释为uint64_t bits,即把bitmap看作bmp64;
  2. 当64 < len时,void* what解释为struct bitmap* what;其中,what[0]就是bmp64_has_0,what[1]就是bmp64_has_1。what[2 + i]就是sub[i]。

确保sizeof(struct bitmap) == sizeof(struct bmp64)就是为了能够让struct bitmap直接退化为struct bmp64而不浪费空间,并且使得这两个结构能混合放入同一个数组中。这个技巧在bitmap.c的实现中可以看到。

bitmap.c

#include <bmp64.h>
#include <bitmap.h>
#include <common.h>

// 当len > 64时,获取下一层的单元长度,
// 当64 < len <= 64 * 64时,sub_maxlen = 64 (双层结构)
// 当64 * 64 < len <= 64 * 64 * 64时,sub_maxlen = 64 * 64 (三层结构)
// 以此类推
#define GET_SUB_MAXLEN(len, sub_maxlen) for((sub_maxlen) = 64; (sub_maxlen) * 64 < (len); (sub_maxlen) *= 64)

// 当len > 64时,对于void* what字段解释
#define BMP64_HAS_0(bitmap) (((struct bmp64*)(bitmap)->what) + 0)
#define BMP64_HAS_1(bitmap) (((struct bmp64*)(bitmap)->what) + 1)
#define SUB_BITMAP(bitmap, index) (((struct bitmap*)(bitmap)->what) + (2 + index))

int bitmap_init(struct bitmap* bitmap, size_t len, int value_all)
{
    assert(bitmap);
    // 这两句断言确保struct bitmap可以“无痛”退化到struct bmp64
    assert(sizeof(struct bmp64) == sizeof(struct bitmap));
    assert(&(((struct bmp64*)0)->len) == &(((struct bmp64*)0)->len));
    if(len == 0)
        ERROR(0, 0, "param <len = %lu> is invalid!", len);
    // 当len <= 64时,直接退化为bmp64
    if(len <= 64)
    {
        int ret = bmp64_init((struct bmp64*)bitmap, len, value_all);
        assert(ret);
        return 1;
    }
    // 下一层最大长度。比如len = 1000,那么sub_maxlen就是64;比如len = 10000,那么sub_maxlen就是4096
    size_t sub_maxlen;
    GET_SUB_MAXLEN(len, sub_maxlen);
    // 下一层有多少个单元。比如len = 1000,那么下一层的最大长度是64,需要16个单元(前15个单元的长度都是64,最后一个长度是40)
    size_t sub_count = (len - 1) / sub_maxlen + 1;
    assert(2 <= sub_count && sub_count <= 64);
    bitmap->len = len;
    bitmap->what = malloc(sizeof(struct bitmap) * (2 + sub_count));
    if(!bitmap->what)
        ERROR(0, 1, "malloc(sizeof(struct bitmap) * (2 + %lu)) failed!", sub_count);
    // 如果都设置为1,那么bmp64_has_0就都是0, 否则都是1
    int ret = bmp64_init(BMP64_HAS_0(bitmap), sub_count, value_all ? 0 : 1);
    assert(ret);
    // 如果都设置为1,那么bmp64_has_1就都是1, 否则都是0
    ret = bmp64_init(BMP64_HAS_1(bitmap), sub_count, value_all ? 1 : 0);
    assert(ret);
    for(size_t i = 0; i < sub_count; i++)
    {
        // 下一层中,前sub_count - 1个单元的长度就是最大长度sub_maxlen,最后一个可能不足
        size_t sub_len = i < sub_count - 1 ? sub_maxlen : len - sub_maxlen * (sub_count - 1);
        // 递归结构!
        int ret = bitmap_init(SUB_BITMAP(bitmap, i), sub_len, value_all);
        assert(ret);
    }
    return 1;
}

// 把逻辑上的index分解为下一层单元的index和单元内的index
#define PARSE_INDEX(bitmap, index, sub_index, index_in_sub)     \
    do                                                          \
    {                                                           \
        size_t sub_maxlen;                                      \
        GET_SUB_MAXLEN((bitmap)->len, sub_maxlen);              \
        (sub_index) = (index) / sub_maxlen;                     \
        (index_in_sub) = (index) & (sub_maxlen - 1);            \
        assert((index_in_sub) == ((index) % sub_maxlen));       \
    }                                                           \
    while(0)

int bitmap_get(struct bitmap* bitmap, size_t index)
{
    assert(bitmap);
    if(index >= bitmap->len)
        ERROR(-1 , 0, "param <index = %lu> is bigger than <bitmap->len = %lu>!", index, bitmap->len);
    // 当len <= 64时,直接退化为bmp64
    if(bitmap->len <= 64)
    {
        int ret = bmp64_get((struct bmp64*)bitmap, index);
        assert(ret != -1);
        return ret;
    }
    size_t sub_index, index_in_sub;
    PARSE_INDEX(bitmap, index, sub_index, index_in_sub);
    // 递归结构!
    int ret = bitmap_get(SUB_BITMAP(bitmap, sub_index), index_in_sub);
    assert(ret != -1);
    return ret;
}

int bitmap_set(struct bitmap* bitmap, size_t index, int value)
{
    assert(bitmap);
    if(index >= bitmap->len)
        ERROR(0 , 0, "param <index = %lu> is bigger than <bitmap->len = %lu>!", index, bitmap->len);
    // 当len <= 64时,直接退化为bmp64
    if(bitmap->len <= 64)
    {
        int ret = bmp64_set((struct bmp64*)bitmap, index, value);
        assert(ret);
        return ret;
    }
    size_t sub_index, index_in_sub;
    PARSE_INDEX(bitmap, index, sub_index, index_in_sub);
    struct bitmap* sub_bitmap = SUB_BITMAP(bitmap, sub_index);
    // 递归结构!
    int ret = bitmap_set(sub_bitmap, index_in_sub, value);
    assert(ret);
    // 修改bmp64_has_0和bmp64_has_1以维护一致性
    // 如果把第i位设置为了1,
    if(value)
    {
        // 那么BMP64_HAS_1的第sub_index位直接设为1,因为sub_bitmap中肯定有1
        bmp64_set(BMP64_HAS_1(bitmap), sub_index, 1);
        // 而0的数量可能会减少,所以需要重新判断
        bmp64_set(BMP64_HAS_0(bitmap), sub_index, bitmap_exist(sub_bitmap, 0));
    }
    // 反之亦然
    else
    {
        bmp64_set(BMP64_HAS_0(bitmap), sub_index, 1);
        bmp64_set(BMP64_HAS_1(bitmap), sub_index, bitmap_exist(sub_bitmap, 1));
    }
    return 1;
}

int bitmap_exist(struct bitmap* bitmap, int value)
{
    assert(bitmap);
    // 当len <= 64时,直接退化为bmp64
    if(bitmap->len <= 64)
        return bmp64_exist((struct bmp64*)bitmap, value);
    // 如果查询是否存在1,只有查询bmp64_has_1中是否存在1
    if(value)
        return bmp64_exist(BMP64_HAS_1(bitmap), 1);
    // 如果查询是否存在0,只有查询bmp64_has_0中是否存在1
    else
        return bmp64_exist(BMP64_HAS_0(bitmap), 1);
}

size_t bitmap_search(struct bitmap* bitmap, int value)
{
    assert(bitmap);
    // 当len <= 64时,直接退化为bmp64
    if(bitmap->len <= 64)
    {
        size_t index = bmp64_search((struct bmp64*)bitmap, value);
        if(index == BMP64_NOT_FOUND)
            return BITMAP_NOT_FOUND;
        return index;
    }
    // 在bmp64_has_<value>中查找到一个1
    size_t sub_index = bmp64_search(value ? BMP64_HAS_1(bitmap) : BMP64_HAS_0(bitmap), 1);
    if(sub_index == BMP64_NOT_FOUND)
        return BITMAP_NOT_FOUND;
    // 在sub[sub_index]中查找一个值为value的位
    size_t index_in_sub = bitmap_search(SUB_BITMAP(bitmap, sub_index), value);
    assert(index_in_sub != BITMAP_NOT_FOUND);
    size_t sub_maxlen;
    GET_SUB_MAXLEN(bitmap->len, sub_maxlen);
    // 计算逻辑上的索引
    return sub_index * sub_maxlen + index_in_sub;
}

浓浓的递归结构啊!

写一个测试:

test_bitmap.c

#include <bitmap.h>
#include <common.h>

int main()
{
    int ret;

    struct bitmap b;
    ret = bitmap_init(&b, 4096, 0);
    assert(ret);

    ret = bitmap_exist(&b, 0);
    assert(ret);

    ret = bitmap_exist(&b, 1);
    assert(!ret);

    ret = bitmap_set(&b, 3500, 1);
    assert(ret);

    ret = bitmap_exist(&b, 0);
    assert(ret);

    ret = bitmap_exist(&b, 1);
    assert(ret);

    for(size_t i = 0; i < 4096; i++)
    {
        size_t index = bitmap_search(&b, 0);
        if(index == BITMAP_NOT_FOUND)
            printf("NOT FOUND! ");
        else
        {
            printf("%lu ", index);
            bitmap_set(&b, index, 1);
        }
    }
    printf("\n");

    return 0;
}
gcc -std=gnu99 bmp64.c bitmap.c test_bitmap.c -o test_bitmap -I.
./test_bitmap

可以看到,从0开始,唯独没有3500,而且最后一次输出“NOT FOUND!”,与预期一致!

然后来测测性能~关闭assert,使用gcc -O3优化,在Intel(R) Xeon(R) Platinum 8180M CPU @ 2.50GHz的服务器上,各个长度级别的操作耗时如下:

位数bitmap_getbitmap_setbitmap_search
643n4ns13ns
409615ns23ns29ns
26214427ns44ns47ns