Redis提供了SETBIT,GETBIT,BITCOUNT,BITOP四个命令用于处理二进制位数组(bit array,又称"位数组").

位数组的表示

使用SDS结构保存位数组,使用SDS的操作函数处理位数组。但是,为了简化SETBIT的实现,保存位数组的顺序和我们平时书写位数组的顺序是相反的,如设置0000 1010,保存在SDS中是0101 0000。

位数组:1111 0000 1100 0011 1010 0101 在SDS中保存为 1010 0101 1100 0011 0000 1111

GETBIT命令的实现

GETBIT <bitarray> <offset>

过程如下:

-1、计算byte = [offset / 8],byte记录了offset偏移量指定的二进制位保存在位数组的哪个字节;
-2、计算bit = (offset mod 8) + 1,bit记录了offset偏移量指定的二进制位是byte字节的第几个二进制位;
-3、根据byte和bit的值,在位数组中定位偏移量指定的二进制位,并返回这个位的值。
复杂度O(1).

SETBIT命令的实现

SETBIT <bitarray> <offset> <value>

1、计算len=[offset / 8] + 1,len记录了保存offset偏移量指定的二进制位至少需要多少字节
2、检查bitarray键保存的位数组长度是否小于len,如果是,扩展SDS长度为len字节,并将所有扩展的长度值置为0
3、计算byte = [offset / 8],byte记录了offset偏移量指定的位保存在哪个字节;
4、计算bit = (offset mod 8) + 1,bit记录了offset偏移量指定的二进制位是byte字节的第几个二进制位
5、根据byte值和bit值,在bitarray键保存的位数组中定位offset指定的二进制位,先记录指定二进制位保存的值到oldvalue,然后将新值设置为这个二进制位的值;
6、向客户端返回oldvalue。
复杂度(O)1;

BITCOUNT命令的实现

BITCOUNT <bitarray>

统计给定位数组中,值为1的二进制位的数量。

二进制位统计算法:

1、遍历算法:,遍历每个二进制位,在遇到值为1的位时,计数器+1;
效率太低,每次循环正检查一个二进制位的值是否为1,检查操作执行的次数与位数组包含的二进制位数量成正比。
2、查表算法:
对于一个有限集合来说,集合元素的排列方式是有限的;
对于一个有限长度的位数组来说,它能表示的二进制位排列也是有限的。
创建一个数字对应位为1个数的表,通过一次读取一个(或多个,需要建更大的映射表)字节,对比数字,就可以知道这个字节有几个位为1。键为数字,值为位上1的个数,查表。
初看起来,只要我们创建一个足够大的表,那么统计工作就可以轻易地完成,但查表法的实际效果会受到内存和缓存两方面因素的限制:

  • 因为查表法是典型的空间换时间策略,节约的时间越多,花费的内存越大。
  • 查表法的效果会受到CPU缓存的限制:对于固定大小的CPU缓存,创建的表越大,CPU缓存所能保存的内容相比整个表越少,那么查表时出现缓存不命中的情况就会越多,缓存换入和换出操作就会越频繁,最终影响查表法的效率。

3、variable-precision SWAR算法
BITCOUNT命令要解决的问题:统计一个位数组中非0二进制位的数量,在数学上被称为“计算韩明重量”。汉明重量经常被用于信息论,编码理论和密码学,所以研究人员开发了多种不同的算法,一些处理器甚至直接带有计算汉明重量的指令,对于不具备这种特殊指令的处理器来说,variable-precision SWAR算法是效率最好的,该算法通过一系列位移和位运算,可以在常数时间内计算多个字节的汉明重量,并且不需要额外的内存。

以下是一个处理32位长度位数组的算法实现:

uint32_t swar(uint32_t i){//步骤1i = (i & 0x55555555) + ((i >> 1) & 0x55555555);//步骤2i = (i & 0x33333333) + ((i >> 2) & 0x33333333);//步骤3i = (i & 0x0f0f0f0f) + ((i >> 4) & 0x0f0f0f0f);//步骤4it = (i * (0x01010101) >> 24);return i
}

执行步骤:

1、计算出值i的二进制标志可以按每两个二进制位为一组进行分组,各组的十进制表示就是改组的汉明重量;

2、计算出的值i的二进制表示可以按每四个二进制位为一组进行分组,各组的十进制表示就是改组的汉明重量;

3、计算出的值i的二进制表示可以按每八个二进制位为一组进行分组,各组的十进制表示就是该组的汉明重量。

4、i*0x01010101计算出bitarray的汉明重量并记录在二进制位的最高八位,而>>24通过右移运算,将bitarray的汉明重量移动到最低八位,得出的结果就是汉明重量。

因为swar函数是单纯的计算操作,所以它无需像查表法那样,使用额外的内存。而且swar函数是一个常数复杂度的操作,所以我们可以按照自己的需要,在一次循环中多次执行swar,从而按倍数提升计算汉明重量的效率。

我们可以按照自己的需要,再一次循环中多次执行swar,从而按倍数提升计算汉明重量的效率:例如,我们在一次循环中调用两次swar函数,那么计算汉明重量的效率就从之前的一次循环计算32位提升到了一次循环计算64位。如果在一次循环中调用四次swar函数,那么一次循环级就可以计算128个二进制的汉明重量,这笔每次循环只调用一次swar函数块4倍。

当然,一次循环执行多个swar调用这种优化方式是有极限的:一旦循环中处理数组的大小超过了缓存大小,这种优化的效果就会降低并最终消失。

variable-precision SWAR逐行解释:

第一行

i = i - ((i >> 1) & 0x55555555);

0x55555555二进制的标识方式如下:

0x55555555 = 0b01010101010101010101010101010101

可以看到的规律是,奇数位为1,偶数位为0。
表达式((i >> 1) & 0x55555555),将i右移一位,并将所有偶数位设置为0.(等效的,我们也可以通过& 0xAAAAAAAA将所有奇数位设置成0,然后再将结果右移1位)为了方便起见,我们将这个中间值命名为j。
当我们将中间值j从原始值i中减去会发生什么?那让我们来看看如果i只有两位是什么情况。

i           j (右移一位)        i - j
----------------------------------
0 = 0b00    0 = 0b00    0 = 0b00
1 = 0b01    0 = 0b00    1 = 0b01
2 = 0b10    1 = 0b01    1 = 0b01
3 = 0b11    1 = 0b01    2 = 0b10

最后的结论就是i-j的十进制结果就是位数组中1出现的次数。
那么如果i不只是两位数组呢?实际上,很容易发现i-j的最低两位仍然如上表所示,三四位,五六位也是一个道理,等等。需要注意的是:

  • 由于& 0x55555555的巧妙用法,尽管>> 1,i-j的最低两位不会被i的第三位或者更高的位影响
  • 由于j的最低两位永远不可能在比i的最低两位大。这个减法永远不会向i的第三位借位,因此:对于i-j来说,i的最低两位不会影响i的第三位或者更高位。

实际上这一行就是将32位数组分为16个两位为单位的组,每组分别计算1出现的次数。
第二行:

i = (i & 0x33333333) + ((i >> 2) & 0x33333333);

与第一行对比,这一行非常的简单。首先,来看一下0x33333333的二进制表示:

0x33333333 = 0b00110011001100110011001100110011

i & 0x33333333的目的是以4位为分组取四位中的后两位。而(i >> 2) & 0x33333333在把i右移两位后做同样的工作。然后把它们结果加起来。
因此,实际上,这行做的工作就是将最低的两位1出现的次数和最低三四位的1出现的次数相加,得到最低四位的1出现的次数。同样的对于输入的8个四位分组(=16进制数)都是一样的。
第三行:

return (((i + (i >> 4)) & 0x0F0F0F0F) * 0x01010101) >> 24;

(i + (i >> 4)) & 0x0F0F0F0F除了这次是用临近的4位1出现的次数系相加,得到8位为一组的1出现的次数,以外原理跟前一行一样。(和上一行有所不同的是,我们可以把&去掉,因为我们知道原始输入8位不可能出现超过8个1因此二进制值不会超过4位。)
现在我们有一个三十二位数,由四个字节组成,每个字节保存着原始输入中为1的位的数量。(我们把它们称作A,B,C和D。)那么为什么我们用0x01010101乘以这个值(命名为k)?

由于:

0x01010101 = (1 << 24) + (1 << 16) + (1 << 8) + 1

可得:

k * 0x01010101 = (k << 24) + (k << 16) + (k << 8) + k

k * 0x01010101最高位就是原始输入的1出现次数的最终结果。>> 24只是简单的将最高位的值移到最低位。

Redis实现

BITCOUNT命令的实现用到了查表和variable-precisionSWAR两种算法:

  • 查表算法使用键长为8位的表,表中记录了从0000 0000 到1111 1111在内的所有二进制位的汉明重量。
  • variable-precisionSWAR算法,BITCOUNT命令在每次循环中载入128个二进制位,然后调用四次32位variable-precision算法来计算这128个二进制位的汉明重量。

程序会根据未处理的二进制位的数量来决定使用哪种算法:
如果未处理的二进制位数量小于128位,那么程序使用查表法来计算二进制位的汉明重量。否则使用variable-precisionSWAR算法来计算二进制位的汉明重量。

BITTOP

因为C语言直接支持对字节执行逻辑与,或,异或,非的操作,所以BITOP的四个操作都是直接基于这些逻辑操作来实现的。
执行过程:
1、创建一个空白的位数组value,用于保存逻辑操作的结果;
2、对两个位数组的每个字节执行逻辑操作,并将结果保存到value[i]字节;
3、经过前面的逻辑操作,程序得到了计算结果,并将它保存在result上。

BITTOP AND <result > <operand-1>  [operand-2]

BITTOP OR <result > <operand-1>  [operand-2]

BITTOP NOT <result > <operand-1>  [operand-2]

BITTOP XOR <result > <operand-1>  [operand-2]

redis(22)--二进制位数组相关推荐

  1. Redis 05_List列表 数组 Hash散列

    First: 基于Linked List实现 元素是字符串类型 列表头尾增删快,中间增删慢,增删元素是常态 元素可以重复出现 最多包含2^32-1元素 列表的索引 从左至右,从0开始 从右至左,从-1 ...

  2. redis获取byte数组_《Redis深度历险》读书笔记

    list/set/hash/zset 这四种数据结构是容器型数据结构: 1.create if not exists 2.drop if no elements 阻塞读在队列没有数据的时候,会立即进入 ...

  3. php redis 存入数组,php redis 多维数组操作

    PHP数组实际上是哈希表,Redis似乎不支持二维数组,但是可以使用hmset把PHP数组保存为hash类型的数据,使用hmget读取一维的键没问题,读取二维的多维的键就返回false. $hset ...

  4. int转char数组_C语言学习第22篇---数组和指针的关系剖析

    数组的本质: 数组是一段连续的内存空间 数组的空间大小为sizeof(array_type)*array_size 数组名可以看做指向数组的第一个元素的常量指针(只是理解,其实是错的,参见<征服 ...

  5. Redis如何保存数组和对象

    https://www.cnblogs.com/xiaozong/p/5666294.html redis类 <?php class MyRedis{private static $handle ...

  6. redis-----07-----redigo基本命令操作(主要讲如何让go的struct、map展开成redis的参数,以及使用struct获取redis返回的key-value批量数组)

    1 请求回应模式 redis 与 client 之间采用请求回应模式,一个请求包对应一个回应包.但是也有例外,pub/sub 模式下,client 发送 subscribe 命令并收到回应包后,之后被 ...

  7. 《redis 设计与实现》读书笔记

    大家好,我是烤鸭:     <redis 设计与实现>,读书笔记. 第一部分 数据结构与对象 第2章 简单动态字符串 Redis 使用SDS 作为字符串表示. O(1) 复杂度获取字符串长 ...

  8. 要看就看最好,16万字全网最硬核redis总结,谁赞成,谁反对?(被粉丝疯狂催更,已有人反馈看完专栏拿到大厂offer!!!)

    我摊牌了,这篇文章,值得99%的人收藏!!! 阅读时间较长,后续会设置成仅粉丝可见,建议收藏起来慢慢看!!! 原创不易,千万不要白嫖,真香警告⚠️. 本文评论区会送出两本我精挑细选的Redis著作,想 ...

  9. Redis最详细讲解

    Redis最详细讲解 原文地址 本文脑图 redis基本数据结构 本文脑图 前言 Redis核心对象 String类型 int SDS SDS与c语言字符串对比 String类型应用 Hash类型 字 ...

最新文章

  1. POJ 3436 ACM Computer Factory
  2. WPF资源的基本概念
  3. Window核心编程
  4. java获得电脑性能_Java:使用SingletonStream获得性能
  5. c语言清空输入缓冲区函数,c语言:C语言清空输入缓冲区在标准输入(stdin)情况 -电脑资料...
  6. 济南大学计算机二级成绩查询,济南大学泉城学院教务系统成绩查询登录入口 jwxt.ujnpl.com/jwglxt/,精英高考网...
  7. php数组去掉键值,PHP 如何将数组去掉键值?_后端开发
  8. dudu注意:这个可能是bug吧?
  9. 巨建华担任BCF理事
  10. Flink WaterMark 详解及结合 WaterMark 处理延迟数据
  11. 个人博客管理系统详解
  12. 手机图形计算器matlab,Mathlab图形计算器
  13. 一些学习网络安全的平台介绍
  14. 美容院如何优化预约服务?
  15. 精华QT安装:qt-opensource-windows-x86-5.14.1
  16. Algo-131-Beaver's Calculator
  17. Android Vitamio
  18. python pandas库详解_Pandas 库的详解和使用补充
  19. 命令行net time同步时间(内网)
  20. oracle 中文导入 乱码 ZHS16GBK AL32UTF8

热门文章

  1. 图解Ubuntu中pidgin登陆IRC
  2. java中的==、equals()、hashCode()源码分析
  3. 通用计划明年推出自动驾驶出租车共享服务,可定制化设计车辆
  4. 代码生成工具之Winform查询列表界面生成
  5. JetBrains 发布Kotlin 1.0
  6. JVM学习 - 体系结构 内存模型
  7. phpmyadmin设置登录密码的几种方法
  8. Android开发学习笔记(10):NDK安装手顺及应用
  9. 2008年最新的100条经典句子
  10. TechEd2007现场侧记:TechEd的变与不变