汉明重量(Hamming Weight)以及 redis的bitcout底层

Hamming Weight,即汉明重量,指的是一个位数组中非0二进制位的数量。


解决问题:

有100M的二进制数据,算出里面有多少个1 ?(1MB = 1,000,000 Byte = 8,000,000 bit)


1、遍历法:

遍历 8 亿次,右移比较 8 亿次。

public int hammingWeight(int n) {
  int res = 0;
  while(n!=0) {
    res+= (n & 0x1);
    n >>>=1;
  }
  return res;
}
2、 查表法:(空间换时间)
keyvalue(值为1的数量)
0000 00000
0000 00011
1111 11118

遍历 1 亿次

空间: 比如java的map<String,integer> 

上述键长为 8 位,那么 2 ^ 8 = 256 ,

String 所需要字节(一个英文字母或者数字占1个字节): 256 * 8 = 2048 byte

Integer 所需要字节 ( 0,1,2,3,4,5,6,7,8)(一个Integer对象粗略统计 3* 4 = 12字节) 9 * 12 = 108 byte

算上map对象总共差不多2500 byte. 



若键的长度持续增加,那么比如16位:

2500 * 2 ^ ( 16 / 8 ) =  2500 * 256 = 625k byte



若是 32位:

2500 * 2 ^ ( 32 / 8 ) = 2500 * ( 2 ^ 24) = 40,000 M byte = 40G



瓶颈 :

1、 内存

2、cpu缓存 :查表之后,会有部分数据打到cpu缓存里面,方便下次使用,但是表格越大,缓存不命中的情况就越高,那么缓存就会不断换入换出,影响性能

3、汉明重量 :variable-precision SWAR 算法

第一步:
  计算出来的值i的二进制可以按每2个二进制位为一组进行分组,各组的十进制表示的就是该组的汉明重量。
第二步:
  计算出来的值i的二进制可以按每4个二进制位为一组进行分组,各组的十进制表示的就是该组的汉明重量。
第三步:
  计算出来的值i的二进制可以按每8个二进制位为一组进行分组,各组的十进制表示的就是该组的汉明重量。
第四步:
  i * (0x01010101)计算出汉明重量并记录在二进制的高八位,>>24语句则通过右移运算,将汉明重量移到最低八位,最后二进制对应的十进制数就是汉明重量。
算法时间复杂度是O(1)的。


// 计算32位二进制的汉明重量

int32_t swar(int32_t i)

{    

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

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

    i = (i & 0x0F0F0F0F) + ((i >> 4) & 0x0F0F0F0F);

    i = (i * (0x01010101) >> 24);

    return i

}



0x55555555 ===>> 0101 0101 0101 0101 0101 0101 0101 0101

0x33333333 ===>> 0011 0011 0011 0011 0011 0011 0011 0011

0x0F0F0F0F ===>> 0000 1111 0000 1111 0000 1111 0000 1111

0x01010101 ===>> 0000 0001 0000 0001 0000 0001 0000 0001



算法解析:


比如: 1101 1010 中有几个 1 ?

a、2个一组拆成 11 01 10 10 ,待会分别处理4组,加起来即可

b、处理 11 ,首先根据最上面的右移遍历法,用 

while(n!=0) {
    res+= (n & 0x1);
    n >>>=1;
  }
即:

11 and 01 = 01 

x= 01 (表示有1个1)

n >>>=1 右移1位,11变成 01
01 and 01 = 01

y= 01 (表示有1个1)

然后 x + y = 10 (表示第一组总共有2个1)



c、剩下3组以此类推 ...


算法核心来了:

一、切成N组,每2位一组 (0x55555555)

于是:处理 0111 1010 0101 0101 0010 0001 1111 0010


0x55555555 ===>> 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01

第一个步骤:

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

即可完成该操作。

先把偶数位的给处理了,然后右移一位把奇数位的给处理了。然后得出的结果加起来 2位一组,每2位就是原始数字每2位中所包含的1的个数。



input: 0111 1010 0101 0101 0010 0001 1111 0010



step1:  

            0011 1101 0010 1010 1001 0000 1111 1001  (i >>> 1)

            0101 0000 0101 0101 0000 0001 0101 0000  (i & 0x55555555)         

            0001 0101 0000 0000 0001 0000 0101 0001 ((i >>> 1) & 0x55555555)

            0110 0101 0101 0101 0001 0001 1010 0001 (+)
            
            01 10 01 01 01 01 01 01 00 01 00 01 10 10 00 01 (拆开)            
            
剖析前面的4位 01 10 ,01表示input 前2位有1个1,10表示继续的俩位有2个1。
那么说明input的前4位有1+2=3个1
  • 接下来的步骤二就是(每2位合并)看下如何把前四位01 10合并起来,把 01 + 10 加起来变成 0011.

二、合并 2位一组的结果 (0x33333333)

  • 4位表示2个2位的相加!!
  1. 看下上面的运算结果,前四位的 0110 ,跟 0011与运算,剩下了后面的 10.
  2. 然后右移2位就只剩下前面的两个数字01,再与0011去与运算,结果是原本前面的 01.
  3. 然后加起来就变成了 0011了
  4. 0011就转换成10进制就代表3个1了。

            0110 0101 0101 0101 0001 0001 1010 0001 ( i )

            0001 1001 0101 0101 0100 0100 0110 1000 ( i>>> 2 )   

            0010 0001 0001 0001 0001 0001 0010 0001 ( i & 0x33333333 )     

            0001 0001 0001 0001 0000 0000 0010 0000 ( ( i >>> 2) & & 0x33333333 )

            0011 0010 0010 0010 0001 0001 0100 0001 (+)

  剖析下前8位 0011 0010 表明,input的前4位有3个1,接下去的4位有2个1,那么前8位总共有3+2=5个1
  • 接下来的步骤就是(每4位合并),比如 0011 0010 这两个4位给加起来

三、合并 4位 一组的结果 (0x0F0F0F0F)

  • 8位表示2个4位的相加!!
  1. 前8位也就是 0011 0010, 2个4位相加 = 0101 5个1 。
  2. 0010 与 1111(0F) 与运算,得到 0010
  3. 右移动4位=0011, 0011 与 1111(F) 与运算,得到 0011
  4. 两者相加 = 0101
step3:

            0011 0010 0010 0010 0001 0001 0100 0001 ( i )

            0000 0011 0010 0010 0010 0001 0001 0100 ( i >>> 4)      

            0000 0010 0000 0010 0000 0001 0000 0001( i & 0x0F0F0F0F)

            0000 0011 0000 0010 0000 0001 0000 0100 ((i >>> 4) & 0x0F0F0F0F))

            0000 0101 0000 0100 0000 0010 0000 0101 (+)

第三步计算完后,分成了四组,每组八个二进制位,只要求出每组上二进制表示的值,相加的结果就是汉明重量。
  • 那么下一个步骤就是要把 0000 0101+ 0000 0100 + 0000 0010 + 0000 0101 ,

四、合并 8位 一组的结果 (0x01010101)

  • i * (0x01010101) >>> 24
  1. 那么还是可以用之前的方法右移8位,然后和 1111 1111 做与运算然后相加,也就是 0xFFFFFFFF。这样子做是把16位合并成8位。
  2. 由于是对32位二进制进行操作,所以还有一个巧妙的方法:由于32位二进制最多就32个1,所以往右移动24位,留下8位,然后4次8位相加即可得到结果。
  3. 那么肯定有人问,为什么剩下8位,8位不是256,应该是留下5位,5位才是32。其实,细心的朋友在看到 step3 的时候,&0x0F0F0F0F 其实每8位的高4位其实都已经是0了,所以最终step3的32位切成的4组8位,都只有低4位去相加,低4位相加,最大也就是1000 +1000 +1000 +1000 = 0010 0000 = 32。
  4. 别问为啥不是4个 1111 相加,因为8位最多就是8个1,也就是 1000。所以看下表g4相加:
g1g2g3g4g5g6g7
...0000 01010000 01000000 00100000 0101
*0000 00010000 00010000 00010000 0001
==============
...0000 01010000 01000000 00100000 0101
..0000 01010000 01000000 00100000 0101.
.0000 01010000 01000000 00100000 0101..
0000 01010000 01000000 00100000 0101...
==============
...0001 0000...

32位二进制与32位二进制的结果是32位。

===> 表明 input 0111 1010 0101 0101 0010 0001 1111 0010 有 16个 1

五、复习一下二进制乘法运算

在这里插入图片描述在这里插入图片描述

六、性能:

  1. 遍历算法一次处理1位,swar函数一次计算32位。
  2. swar 函数计算32位二进制位的汉明重量,比遍历算法快32倍。比键长为8位的查表法快4倍。比键长为16位的查表法快2倍。但是他不用耗费内存,因为他就单纯计算。
  3. swar是一个常熟复杂度的操作,所以可以在一次循环中多次执行 swar 函数,如一次循环调用两次 swar 函数,那么一次循环可以计算64位。性能是原来的2倍。调用四次swar,那么一次循环就可以计算128位,性能是原来的4倍。但是是有极限的,当循环中处理的数组的大小超过cpu缓存的大小,这种优化效果会降低并消失。

七、扩展:

redis 的 bitcount 底层算法


查表法(上述2,键长8位) + swar ( 1次循环4次swar,一次处理128位)

那么策略就是:

当一个二进制长度小于等于128位(8个二进制)的时候,用查表法。

当一个二进制长度大于128位的时候,用swar。



此时计算一个 100 M = 800 000 000 bit来计算,需要第一轮循环 800 000 000 / 128 = 6 250 000次。
第二轮循环 0次。如果不整除,那么mod,余数进行第二轮循环去查表法解决。

第一轮循环执行次数 loop1 = n / 128

第二轮循环执行次数 loop2 = n mod 128

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值