在日常的开发中,接触的各种库中,经常会出现行业内约定俗成的内容,比如:初始化一个容器长度的时候,要保证容器的长度为 2 的 n 次方。常见的如 Java hashmap 初始化的时候,其底层实现的时候一定会保证初始化的数组一定是 2 的 n 次方。golang 中常用的第三方库 bigcache,在初始化分片数的时候,默认也一定会保证分片数是 2 的 n 次方。
2的n次方的作用
经常出现的这些2的n次方的数字有什么作用呢?根据我的经验来看,主要是方便计算。长度为2的n次方的容器出现的时候,往往是伴随着 hash 算法,如 Java hashmap 和 golang bigcache 都是如此,即如何将一个元素定位到 hash 表的位置。
计算一个元素在 hash 表中的位置,最常见的作法就是取余,即先计算元素的 hash code,再对 hash 表的长度取余,余数便是元素在 hash 表中的位置。但取余运算在计算机中其实是很慢的,而常见的二进制运算如 与运算、或运算、异或运算、移位运算等是很快的。所以是否可以用这些快速的运算来替换取余运算,而又能得到完全一样的结果,也就成为了优化的目标。
2 的 n 次方数字就能很好实现上面目标。如一个长度为 16 的 hash 表,插入一个 hash code 为 5 的元素,两种等价的实现如下:
取余:5 % 16 = 5
与运算:5 & (16 - 1)= 5
对比一下非 2 的 n 次方的数字,如果 hash 表的长度为 15,插入一个 hash code 为 5 的元素:
取余:5 % 15 = 5
与运算:5 & (15 - 1)!= 5
如果是非 2 的 n 次方的数字,取余也就只能采用取余运算,无法用其他快速的运算来达到同样的目的。
判断是否 2 的 n 次方
如何判断一个数字是否 2 的 n 次方呢?最长想到的就是这个数字不停的除以 2 并 对 2 取余,但凡哪一次取余的结果不为 0,那么这个数字就不是 2 的 n 次方。
根据上面的判断过程能看得出来,计算效率很低。如何提高效率,也要用到更快的运算方式,如一个 16 为例,判断是否 2 的 n 次方:
16 &(16 - 1)= 0
这种方法很巧妙,一个数字如果是 2 的 n 次方,那么最高位是 1,其他位全是 0。如果将这个数字减去 1,那么成为了最高位是 0,其他位数全是 1。