感兴趣的是variable-precision SWAR算法,因为看书进度问题没研究透彻,未完待研究!
22.0 二进制位数组
redis提供了四个命令处理二进制数位组:SETBIT、GETBIT、BITCOUNT、BITOP四个命令处理二进制位数组
- SETBIT命令为位数组指定偏移量上的二进制位设置值,位数组的偏移量从0开始计数,而二进制位的值可能是0或者1
- SETBIT bit 0 1
- SETBIT bit 3 1
- GETBIT获取位数组指定偏移量伤的二进制位
- GETBIT bit 0
- BITCOUNT命令用于统计位数组里面,值为1的二进制位的数量
- BITCOUNT bit
- BITOP可以对多个位数进行按位与、按位或、按位异或、给定的数组进行取反操作
- BITOP AND and-result x y z
- BITOP OR or-result x y z
- BITOP XOR xor-result x y z
- BITOP NOT not-result x
22.1 位数组的表示
- Redis使用字符串对象来表示位数组,使用逆序保存位数组简化SETBIT的实现
- sds表示一字节长的位数组,举例
- redisObject里面的type设置为REDIS_STRING
- sdshdr.len=1,表示保存了一字节长的数组
- buf数组的buf[0]保存一字节长的位数组
- buf数组的buf[1]保存sds自动追加到值末尾的空字符’\0’
//code0 server.h
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
* LFU data (least significant 8 bits frequency
* and most significant 16 bits access time). */
int refcount;
void *ptr;
} robj;
//sds.h
typedef char *sds;
/* Note: sdshdr5 is never used, we just access the flags byte directly.
* However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* used */
uint8_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len; /* used */
uint16_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
22.2 GETBIT命令的实现
- GETBIT
- 执行过程
- 计算byte:计算offset偏移量指定的二进制位保存在位数组的哪个字节
- 计算bit:计算bitarray中定位offset偏移量指定的二进制位,并返回这个位的值
- 定位到buf[?]字节,去除对应的二进制位
22.3 SETBIT命令的实现
- SETBIT
- 执行过程
- 计算offset偏移量执行的二进制位至少需要多少字节len=(offset/8)+1
- 检查bitarray保存的位数组长度是否小于len,如果是将sds长度扩展成len字节,新扩展的空间的二进制位设置为0
- 计算byte,offset偏移量指定的二进制位保存在位数组的哪个字节
- 计算bit,算bitarray中定位offset偏移量指定的二进制位
- 根据byte和bit找到指定二进制位现在的值,保存为oldvalue;然后将新值value设置为这个二进制位的值
- 向客户端返回oldvalue
- 逆序保存的好处:
- 因为buf使用逆序来保存位数组,所以程序对buf数组进行扩展之后,写入操作可以直接在新扩展的二进制位中完成,不用改动原有的位置
- 如果是正序,每次扩展需要移位才能写入,速度慢且实现复杂
22.4 BITCOUNT命令的实现
- 二进制位统计算法(1):遍历算法
- 二进制位统计算法(2):查表算法
- 创建键长为8位的表仅需数百个字节,键长16位的仅需百个KB,键长32位的需要十多个GB
- 实际中,内存大小和CPU缓存大小会影响查表效率。
- 一般服务器只能接受数百个字节或者数百KB的内存消耗
- 创建表格越大,CPU缓存能保存的内容相比整个表格比例会越少,缓存不命中的比例就会越高
- 二进制位统计算法(3):variable-precision SWAR
- 问题:统计一个位数组中非0二进制位的数量,数学中称之为“计算汉明重量”
- variable-precision SWAR算法:通过一系列位移和位运算操作,可以在常数时间内计算多个字节的汉明重量,不需要额外的内存,以处理32位长度位数组为例子
- 计算每两个二进制位为一组,各组的十进制就表示该组的汉明重量
- 计算每四个二进制位为一组,各组的十进制就表示该组的汉明重量
- 计算每八个二进制位为一组,各组的十进制就表示该组的汉明重量
- 计算汉明重量并记录在最高八位,右移24位,得到汉明重量
- 二进制位统计算法(4):Redis实现
- 未处理的二进制位小于128位,使用查表法计算汉明重量(8bits查表)
- 未处理的二进制位大于128位,使用variable-precision SWAR算法计算汉明重量(4次32位计算,然后再下一个128位)
22.5 BITOP命令的实现
- command
- BITOP AND and-result x y z
- BITOP OR or-result x y z
- BITOP XOR xor-result x y z
- BITOP NOT not-result x
- 时间复杂度
- AND OR XOR可接受多个位数组作为输入,时间复杂度位n^2
- NOT只能接受一个位数组输入,时间复杂度N