redis源码浅析--二十.BIT MAP的实现

环境说明:redis源码版本 5.0.3;我在阅读源码过程做了注释,git地址:https://gitee.com/xiaoangg/redis_annotation
如有错误欢迎指正
参考书籍:《redis的设计与实现》

推荐阅读:
redis-Bitmaps 基础概念

redis简单动态字符串SDS

汉明重量

文章推荐:
redis源码阅读-一--sds简单动态字符串
redis源码阅读--二-链表
redis源码阅读--三-redis散列表的实现
redis源码浅析--四-redis跳跃表的实现
redis源码浅析--五-整数集合的实现
redis源码浅析--六-压缩列表
redis源码浅析--七-redisObject对象(下)(内存回收、共享)
redis源码浅析--八-数据库的实现
redis源码浅析--九-RDB持久化
redis源码浅析--十-AOF(append only file)持久化
redis源码浅析--十一.事件(上)文件事件
redis源码浅析--十一.事件(下)时间事件
redis源码浅析--十二.单机数据库的实现-客户端
redis源码浅析--十三.单机数据库的实现-服务端 - 时间事件
redis源码浅析--十三.单机数据库的实现-服务端 - redis服务器的初始化
redis源码浅析--十四.多机数据库的实现(一)--新老版本复制功能的区别与实现原理
redis源码浅析--十四.多机数据库的实现(二)--复制的实现SLAVEOF、PSYNY
redis源码浅析--十五.哨兵sentinel的设计与实现
redis源码浅析--十六.cluster集群的设计与实现
redis源码浅析--十七.发布与订阅的实现
redis源码浅析--十八.事务的实现
redis源码浅析--十九.排序的实现
redis源码浅析--二十.BIT MAP的实现
redis源码浅析--二十一.慢查询日志的实现
redis源码浅析--二十二.监视器的实现

目录

一 位数组的表示

二 GETBIT命令的实现

三 SETBIT命令的实现

四 BITCOUNT 命令的实现

1.遍历算法

2.查表法

3 variable-precision SWAR 算法

五 BITOP命令的实现


redis提供了SETBIT、GETBIT、BITCOUNT、BITOP 、BITFIELD(起始版本3.2.0)、BITPOS 等命令处理二进制位数组;

一 位数组的表示

redis使用字符串对象来表示位数组;因为SDS是二进制安全的,所以程序直接使用SDS结构来保存位数组,并使用SDS的操作函数 实现位操作;

Bitmaps本身不是一种数据结构, 实际上它就是字符串;

二 GETBIT命令的实现

getbit命令返回位数组,在offset偏移量上的值,注意offset是从0开始

getbit命令语法;

GETBIT <KEY> <OFFSET>

因为bitmap的本质其实字符串, getbit命令的核心是将offset转换成对应的byte位置和bit位置;

gitbit命令的实现入口位于bitop.c/getbitCommand

/**
 * GETBIT 命令的实现
 */ 
/* GETBIT key offset */
void getbitCommand(client *c) {
    robj *o;
    char llbuf[32];
    size_t bitoffset; //
    size_t byte, bit;
    size_t bitval = 0;

    //解析命令中的offset 
    if (getBitOffsetFromArgument(c,c->argv[2],&bitoffset,0,0) != C_OK)
        return;

    //查找命令中的key ,并检查编码方式是否是 OBJ_STRING
    if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.czero)) == NULL ||
        checkType(c,o,OBJ_STRING)) return;

    //byte = bitoffset/8; 记录偏移量位于哪个字节
    byte = bitoffset >> 3;
    //bit对应的位, 举个列子 如 bitoffset是5,则取的值从右数第第5位,左数第2位 
    bit = 7 - (bitoffset & 0x7);
    if (sdsEncodedObject(o)) {
        if (byte < sdslen(o->ptr))
            bitval = ((uint8_t*)o->ptr)[byte] & (1 << bit);
    } else {
        if (byte < (size_t)ll2string(llbuf,sizeof(llbuf),(long)o->ptr))
            bitval = llbuf[byte] & (1 << bit);
    }

    addReply(c, bitval ? shared.cone : shared.czero);
}

 

三 SETBIT命令的实现

setbit命令用于将位数组上,偏移量设置成指定的值value;
语法:

SETBIT <key> <offset> <value>

setbit命令执行过程如下:

  1. 计算offset对应的byte位和bit位;
  2. 检查offset是否超过了当前SDS的长度,如果超出了,将扩容SDS,并初始化扩容部分为0;
  3. 获取当前offset位的值;
  4. 设置offset位 的新的值,并返回老的值;

setbit命令的实现 bitop.c/setbitCommand

/**
 * SETBIT命令的实现
 */ 
/* SETBIT key offset bitvalue */
void setbitCommand(client *c) {
    robj *o;
    char *err = "bit is not an integer or out of range";
    size_t bitoffset;
    ssize_t byte, bit;
    int byteval, bitval;
    long on;

    //获取 offset 参数
    if (getBitOffsetFromArgument(c,c->argv[2],&bitoffset,0,0) != C_OK)
        return;

    //获取 key参数 对象
    if (getLongFromObjectOrReply(c,c->argv[3],&on,err) != C_OK)
        return;

    //on & ~1 为真时候,说明 on 不是 0 或者 1; 说明参数错误 
    /* Bits can only be set or cleared... */
    if (on & ~1) {
        addReplyError(c,err);
        return;
    }

    //查找key 并判断offset是否在超过了key sds的长度,
    if ((o = lookupStringForBitCommand(c,bitoffset)) == NULL) return;

    //获取 offset为当前的值
    /* Get current values */
    byte = bitoffset >> 3;
    byteval = ((uint8_t*)o->ptr)[byte];
    bit = 7 - (bitoffset & 0x7);
    bitval = byteval & (1 << bit);

    /**
     * 设置offset位为新值
     */ 
    /* Update byte with new bit value and return original value */
    byteval &= ~(1 << bit);
    byteval |= ((on & 0x1) << bit);
    ((uint8_t*)o->ptr)[byte] = byteval;

    //通知key发生变更
    signalModifiedKey(c->db,c->argv[1]);
    notifyKeyspaceEvent(NOTIFY_STRING,"setbit",c->argv[1],c->db->id);
    server.dirty++;

    //返回客户端原来的值;
    addReply(c, bitval ? shared.cone : shared.czero);
}

四 BITCOUNT 命令的实现

bitcount用于统计给定位数组中 ,值位1的二进制位数量;

语法:

BITCOUNT key [start] [end]

bitcount乍看上去不复杂,但是要高效的的实现这个命令并不容易;

1.遍历算法

实现bitcount的最简单的方法就是 遍历数组中的每个二进制位,遇到值为1的,将计数器+1;

遍历算法实现简单,但效率极低;1MB = 1024^2  Byte; 统计1MB的字符,就要8* 1024^2 bit操作;

2.查表法

查表法原理

  • 对于个有限集合,集合的排列方式是有限的;
  • 对于一个有限长度的位数组来说,他能表示的二进制位排列也是有限;

根据这个原理,我们可以创建一个表,表的键是某种排列的位数组,值是对应位值为1的数量;

举个例子,我们创建下面一个8位长的位数组表
通过这个表我们可以一次性读入8位 进行查表,就能直接得出这个值包含1的数量;

优点:
对比遍历算法,我们一次性就能统计8位二进制位,性能提升了8倍;

如果我们建个更大的表,一次性检查的位就更多了;

缺点:
乍看 只要建立一个更大表,但是查表发会受到内存和缓存两方面的限制:

  • 查表法是典型的空间换时间策略,节约时间越多,花费的额外内存就越大;

    创建8位的表需要几百个字节,创建16位的需要几百KB,创建32位的表,则需要十多个G。
    服务器能接受的内存消耗是有限的;
  • cpu缓存的限制;

    对于固定大小的cpu缓存来说,创建表越大,cpu缓存能保存的内容 相比整个表比例就越少;
    查表出现缓存不命中(cache miss)就越高,缓存的换入和换出操作就越频繁,影响查表法实际效率;
     

结论:
查表法相比遍历法效率更高,但受到内存大小和cpu缓存的限制,对于处理非常长的位数组,效率远远不够;

 

3 variable-precision SWAR 算法

 汉明重量
汉明重量是一串符号中非零符号的个数。因此它等同于同样长度的全零符号串的汉明距离。在最为常见的数据位符号串中,它是1的个数。

可以看到 bitcount要解决的问题 在数学上被称为汉明重量。 目前已知效率最好的解法是是variable-precision SWAR 算法;


/**
 * 
 * 统计二进制数组中由“s”和长“Count”字节指向的位数。
 * 该函数的实现需要使用输入字符串长度达到512 MB。
 * 
 * s:统计字符起始位置
 * count:总的byte数量,例如 count=8,从 s开始,共统计8 byte
 * 
 * //TODO “汉明重量” sware算法
 * https://segmentfault.com/a/1190000015481454
 */ 
/* Count number of bits set in the binary array pointed by 's' and long
 * 'count' bytes. The implementation of this function is required to
 * work with a input string length up to 512 MB. */
size_t redisPopcount(void *s, long count) {
    size_t bits = 0; //计数器 统计1的数量
    unsigned char *p = s;
    uint32_t *p4;
    
    //键长8位的hash表,记录所有 0000 0000 到1111 1111 的汉明重量
    static const unsigned char bitsinbyte[256] = {0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4,1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,4,5,5,6,5,6,6,7,5,6,6,7,6,7,7,8};

    /**
     * //TODO p & 3 啥意思 四字节对齐?
     */ 
    /* Count initial bytes not aligned to 32 bit. */
    while((unsigned long)p & 3 && count) {
        bits += bitsinbyte[*p++];
        count--;
    }

    /**
     * 一次性统计 28byte的 
     */
    /* Count bits 28 bytes at a time */
    p4 = (uint32_t*)p;
    while(count>=28) {
        uint32_t aux1, aux2, aux3, aux4, aux5, aux6, aux7;

        aux1 = *p4++;
        aux2 = *p4++;
        aux3 = *p4++;
        aux4 = *p4++;
        aux5 = *p4++;
        aux6 = *p4++;
        aux7 = *p4++;
        count -= 28;

        aux1 = aux1 - ((aux1 >> 1) & 0x55555555);
        aux1 = (aux1 & 0x33333333) + ((aux1 >> 2) & 0x33333333);
        aux2 = aux2 - ((aux2 >> 1) & 0x55555555);
        aux2 = (aux2 & 0x33333333) + ((aux2 >> 2) & 0x33333333);
        aux3 = aux3 - ((aux3 >> 1) & 0x55555555);
        aux3 = (aux3 & 0x33333333) + ((aux3 >> 2) & 0x33333333);
        aux4 = aux4 - ((aux4 >> 1) & 0x55555555);
        aux4 = (aux4 & 0x33333333) + ((aux4 >> 2) & 0x33333333);
        aux5 = aux5 - ((aux5 >> 1) & 0x55555555);
        aux5 = (aux5 & 0x33333333) + ((aux5 >> 2) & 0x33333333);
        aux6 = aux6 - ((aux6 >> 1) & 0x55555555);
        aux6 = (aux6 & 0x33333333) + ((aux6 >> 2) & 0x33333333);
        aux7 = aux7 - ((aux7 >> 1) & 0x55555555);
        aux7 = (aux7 & 0x33333333) + ((aux7 >> 2) & 0x33333333);
        bits += ((((aux1 + (aux1 >> 4)) & 0x0F0F0F0F) +
                    ((aux2 + (aux2 >> 4)) & 0x0F0F0F0F) +
                    ((aux3 + (aux3 >> 4)) & 0x0F0F0F0F) +
                    ((aux4 + (aux4 >> 4)) & 0x0F0F0F0F) +
                    ((aux5 + (aux5 >> 4)) & 0x0F0F0F0F) +
                    ((aux6 + (aux6 >> 4)) & 0x0F0F0F0F) +
                    ((aux7 + (aux7 >> 4)) & 0x0F0F0F0F))* 0x01010101) >> 24;
    }
    
    //统计剩余的byte
    /* Count the remaining bytes. */
    p = (unsigned char*)p4;
    while(count--) bits += bitsinbyte[*p++];
    return bits;
}

五 BITOP命令的实现

BITOP对一个或多个保存二进制位的字符串 key 进行位元操作,并将结果保存到指定key上;

语法:

BITOP <op_name> <target_key> <src_key1> [src_key2 src_key3 ... src_keyN] 

op_name可以是 AND 、 OR 、 NOT 、 XOR 这四种操作中的任意一种:

C语言支持直接对字节执行 &(逻辑与)、|(逻辑或)、^(异或)、~(逻辑非)

redis bitop全都是基于这些操作实现的;

因为 AND 、OR、XOR 支持多个key操作,并且需要遍历每个key的每个字节进行操作,所以时间复杂度是O(n*n);

NOT操作支持一个key,所以时间复杂度是O(n);

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值