什么是位运算
下面以一张图来演示一下位运算规则
位运算快在哪里?
位运算真的快了吗?
下面以左移一位与乘以2来实验一下
public void multiply2_1() {
int i = 1;
i = i << 1;
}
public void multiply2_2() {
int i = 1;
i *= 2;
}
优化分为两部分,一个是编译器优化,另一个是处理器优化
编译好之后,用javap -c来看下编译好的class文件,字节码是:
public void multiply2_1();
Code:
0: iconst_1
1: istore_1
2: iload_1
3: iconst_1
4: ishl
5: istore_1
6: return
public void multiply2_2();
Code:
0: iconst_1
1: istore_1
2: iload_1
3: iconst_2
4: imul
5: istore_1
6: return
可以看出左移是ishl,乘法是imul,从字节码上看编译器并没有优化。那么在执行字节码转换成处理器命令是否会优化呢?是会优化的,在底层,乘法其实就是移位,但是并不是简单地左移
测试结果:
Benchmark Mode Cnt Score Error Units
BitUtilTest.multiply2_n_mul_not_overflow thrpt 300 35882831.296 ± 48869071.860 ops/s
BitUtilTest.multiply2_n_shift_not_overflow thrpt 300 59792368.115 ± 96267332.036 ops/s
可以看出,左移位相对于乘法还是有一定性能提升的
同理来说,右移位相对于除法也是有一定性能提升的
下面讲的 “取余”与“取与”运算 更有一定提升
结合jdk类底层分析
HashMap为例,有一个典型的例子,计算key对应的数组下标时,用位运算来代替求余操作。
上源码
/**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
length must be a non-zero power of 2,注意到注释里面的英文没有?
当容量是2^n时,h & (length - 1) == h % length 这个表达式就成立了。
是非常好证明的
位运算效率非常高。在《编程珠玑》这本书里,曾说过模运算花费的时间大概是算术运算的10倍左右。
这个是一个很经典的位运算运用,广泛用于各种高性能框架。例如在生成缓存队列槽位的时候,一般生成2的n次方个槽位,因为这样在选择槽位的时候,就可以用取与代替取余;java中的ForkJoinPool的队列长度就是定为2的n次方;netty中的缓存池的叶子节点都是2的n次方,当然这也是因为是平衡二叉查找树算法的实现。
进一步
上文说到2的n次方是个好东西,我们在写框架的很多时候,想让用户传入一个必须是2的n次方的参数来初始化某个资源池,但这样不是那么灵活,我们可以通过用户传入的数字N,来找出不大于N的最大的2的n次方,或者是大于N的最小的2的N次方。
抽象为比较直观的理解就是,找一个数字最左边的1的左边一个1(大于N的最小的2的N次方),或者是最左边的1(小于N的最大的2的N次方),前提是这个数字本身不是2的n次方。
那么,如何找呢?一种思路是,将这个数字最高位1之后的所有位都填上1,最后加一,就是大于N的最小的2的N次方。右移一位,就是小于N的最大的2的N次方。
如何填补呢?可以考虑按位或计算,我们知道除了0或0=0以外,其他的都是1. 我们现在有了最左面的1,右移一位,与原来按位或,就至少有了两位是1,再右移两位并按位或,则至少有四位为1。。。以此类推:
来一张图给大家理解:
老生常谈的HashMap
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
HashMap的hash计算也用到了位运算。还有集合类的扩容也经常用到了位运算。
同样在并发中,也有很多地方用到了位运算,而且很巧妙,比如下面这两个代表。
ThreadPoolExecutor中ctl变量的设计确实精美,用高3位表示线程池的运行状态,低29位表示线程池中线程数。
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
// Packing and unpacking ctl
private static int runStateOf(int c) { return c & ~CAPACITY; }
private static int workerCountOf(int c) { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }
同样的设计,还有读写锁ReentrantReadWriteLock内部的同步容器框架Sync中的读写状态state,分成高16位与低16位,其中高16位表示读锁个数,低16位表示写锁个数。
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
/** Returns the number of shared holds represented in count */
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
/** Returns the number of exclusive holds represented in count */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
除此之外,还有我们常用的包装类Integer 、Long等,里面就有很多位操作。比如这些方法highestOneBit、lowestOneBit、numberOfLeadingZeros、bitCount、reverse等。依次类推还有很多跟数学相关的类都用到了。
最后,还得要提一个类BitSet,里面有很多位运算的操作,在一些海量数据处理的时候,可能用得着。
实战
除去底层,框架设计方面,应用在算法方面呢?
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
示例 1:
输入: [2,2,1]
输出: 1
示例 2:
输入: [4,1,2,1,2]
输出: 4
上代码
class Solution {
public int singleNumber(int[] nums) {
int single = 0;
for (int num : nums) {
single ^= num;
}
return single;
}
}
利用异或特性
利用位运算 此外还有很多平时的小技巧
计算奇偶,二进制1的个数,交换2个数等等
进阶
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现了三次。找出那个只出现了一次的元素。
总结
写了这么多,我只想表达一个观点。有的程序员过于关注于代码的效率,导致编写出来的代码晦涩难懂,后来者难以维护;而有的程序员基本上不关注程序效率,虽然代码结构清晰漂亮,但效率极低。要想变得优秀,应该多读前人的代码,要做到两者兼顾,不能盲目调优,写这种位运算的代码的时候,切记,写注释!