什么是位运算

下面以一张图来演示一下位运算规则

java 使用 compose_ico


位运算快在哪里?

位运算真的快了吗?

下面以左移一位与乘以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 这个表达式就成立了。

是非常好证明的

java 使用 compose_算法_02

java 使用 compose_位运算_03

位运算效率非常高。在《编程珠玑》这本书里,曾说过模运算花费的时间大概是算术运算的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。。。以此类推:

来一张图给大家理解:

java 使用 compose_位运算_04

老生常谈的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个数等等

进阶

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现了三次。找出那个只出现了一次的元素。


总结

写了这么多,我只想表达一个观点。有的程序员过于关注于代码的效率,导致编写出来的代码晦涩难懂,后来者难以维护;而有的程序员基本上不关注程序效率,虽然代码结构清晰漂亮,但效率极低。要想变得优秀,应该多读前人的代码,要做到两者兼顾,不能盲目调优,写这种位运算的代码的时候,切记,写注释!