为啥Java中的BitSet使用long数组做内部存储,而不使用int数组...

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了为啥Java中的BitSet使用long数组做内部存储,而不使用int数组...相关的知识,希望对你有一定的参考价值。

JDK选择long数组作为BitSet的内部存储结构是出于性能的考虑,因为BitSet提供and和or这种操作,需要对两个BitSet中的所有bit位做and或者or,实现的时候需要遍历所有的数组元素。使用long能够使得循环的次数降到最低,所以Java选择使用long数组作为BitSet的内部存储结构。

从数据在栈上的存储来说,使用long和byte基本是没有什么差别的,除了编译器强制地址对齐的时候,使用byte最多会浪费7个字节(强制按照8的倍数做地址对其),另外从内存读数组元素的时候,也是没有什么区别的,因为汇编指令有对不同长度数据的mov指令。所以说,JDK选择使用long数组作为BitSet的内部存储结构的根本原因就是在and和or的时候减少循环次数,提高性能。
作者:马天笑
链接:http://www.zhihu.com/question/21061816/answer/17047941
来源:知乎

当对单个bit进行操作时,两种存储方式不会有太大的区别:首先计算bit所在的word,然后对word中对应的bit进行操作。当同时对多个bit进行操作时,long数组存储方式可以带来较大的新能提升。例如我们进行BitSet中的and, or, xor操作时,要对整个bitset中的bit都进行操作,需要依次读出bitset中所有的word,如果是long数组存储,我们可以每次读入64个bit,而int数组存储时,只能每次读入32个bit。另外我们在查找bitset中下一个置为1的bit时,word首先会和0进行比较,如果word的值为0,则表示该word中没有为1的bit,可以忽略这个word,如果是long数组存储,可以一次跳过64个bit,如果是int数组存储时,一次只能跳过32个bit。

作者:王强
链接:http://www.zhihu.com/question/21061816/answer/60149117
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
参考技术A 1、int是“整型”long是“长整形”
2、long int占4个字节,int占2个字节
3、int存储的整数的值域小long。(前三条没多大影响只是区别,主要在于下面两条)
4、BitSet提供and和or这种操作
5、使用long能够使得循环的次数降到最低,所以Java选择使用long数组作为BitSet的内部存储结构,来提高性能。
参考技术B int一般是32位,long是64位,存的多呀,同时现在处理器也是64位的,那么使用long能减少从内存读入cpu的次数吧 参考技术C long有64位,int32位,long装的数据多呗

一分钟轻松掌握 !Java 高级数据结构 -- 原生 BitSet 源码刨析

为啥要肝 Java 原生 BitSet

  今天,来聊下关于 java.util 包中的 BitSet 。为什么突然聊这个呢?是有下面一个情景:

  原 AbTest 平台进行 Go 版本的重构「重构的原因有很多,比如资源利用率低、Java 多线程切 Go 协程、等等,这里不做重点」,原实验组流量配比的随机性分桶是用 Java 原生包 BitSet 存储,重构需要完全契合 BitSet 的映射策略。

  所以这里唠一下 Java 原生 BitSet 的内部实现,结尾也会附上 Go 版的实现!

BitSet 介绍

  BitSet ,又叫做 BitMap 。包含于 java.util 原生包,一种数据结构,通过 bit 的两种状态「 true / false 」标识数据性质,通常在大数据排序、优化大数据存储、等等。

  像上文提到的,就是 利用 BitSet 存储了实验流量分配的大量命中信息。
  将 AbTest 实验随机分配的流量位存储在 BitSet 中,当流量进入的时候,依据 BitSet 中的信息进行命中判断,进而完成实验的相关配置。

BitSet 使用

  BitSet 的使用很简单,像 Java 普通类一样,New 之后,进行初始化操作。
  这里提供了多种初始化方法,比如:

static BitSet
valueOf(byte[] bytes)
Returns a new bit set containing all the bits in the given byte array.
static BitSet
valueOf(ByteBuffer bb)
Returns a new bit set containing all the bits in the given byte buffer between its position and limit.
static BitSet
valueOf(long[] longs)
Returns a new bit set containing all the bits in the given long array.
static BitSet
valueOf(LongBuffer lb)
Returns a new bit set containing all the bits in the given long buffer between its position and limit.

  可以观察出来,这些只是参数不一致,就是多态中的重载

  我们将不以这些方法作为 BitSet 内部机制的分析载体,而是以其中的数据存储方法 Set 作为入口,更具有代表性,一致性。

Set 偷窥真相的入口

  Set 方法是 BitSet 进行数据写入的入口方法,可完成指定位置的数据写入。
  方法介绍如下:

public void set(int bitIndex)
Sets the bit at the specified index to true.
Parameters:
bitIndex - a bit index
Throws:
IndexOutOfBoundsException - if the specified index is negative
Since:
JDK1.0

Set 源码

  了解内部机制,主要两种方法,一种是源码追溯,一种文档查阅。
  下面我们从 Set 源码入手:

public void set(int bitIndex) {
        if (bitIndex < 0)
            throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);

        int wordIndex = wordIndex(bitIndex);
        expandTo(wordIndex);
        words[wordIndex] |= (1L << bitIndex); // Restores invariants
        checkInvariants();
    }
    private void expandTo(int wordIndex) {
        int wordsRequired = wordIndex+1;
        if (wordsInUse < wordsRequired) {
            ensureCapacity(wordsRequired);
            wordsInUse = wordsRequired;
        }
    }
   // 扩容操作,2倍扩容
    private void ensureCapacity(int wordsRequired) {
        if (words.length < wordsRequired) {
            // Allocate larger of doubled size or required size
            int request = Math.max(2 * words.length, wordsRequired);
            words = Arrays.copyOf(words, request);
            sizeIsSticky = false;
        }
    }

    /**
     * Every public method must preserve these invariants.
     */
    private void checkInvariants() {
        assert(wordsInUse == 0 || words[wordsInUse - 1] != 0);
        assert(wordsInUse >= 0 && wordsInUse <= words.length);
        assert(wordsInUse == words.length || words[wordsInUse] == 0);
    }

  我们看 set (int bitIndex) 方法,很简单,不到十行的代码!
  画了主要流程图,如下:

  大体可分为下面四部分,核心为二、四步骤

1、参数有效性的校验,无效参数直接抛出异常「IndexOutOfBoundsException」;

2、计算数组下标;

3、数组扩容操作;

4、数组元素位操作;

计算 word 数组下标

  下标 wordIndex 计算源码如下,其将参数 bitIndex 进行了右移 6 位操作,相当于 bitIndex / 2^6 (64) 。

/*
* BitSets are packed into arrays of "words."  Currently a word is
* a long, which consists of 64 bits, requiring 6 address bits.
* The choice of word size is determined purely by performance concerns.
*/
private final static int ADDRESS_BITS_PER_WORD = 6;

/**
 * Given a bit index, return word index containing it.
 */
private static int wordIndex(int bitIndex) {
    return bitIndex >> ADDRESS_BITS_PER_WORD;
}

  为什么这样做呢?和 bitSet 数据结构有关系!

  这里引入 bitset 的初始化源码:

   public BitSet(int nbits) {
        // nbits can‘t be negative; size 0 is OK
        if (nbits < 0)
            throw new NegativeArraySizeException("nbits < 0: " + nbits);

        initWords(nbits);
        sizeIsSticky = true; //标识words大小由用户指定
    }
   // 
    private void initWords(int nbits) {
        words = new long[wordIndex(nbits-1) + 1];
    }

  可以看到,BitSet 其实质数据结构为 Long[] ,也就是 64 位的元素集合,每个元素也都是 64 位,如下图:

  bitIndex 需要进行 64 位的偏移找到所在的数组下标。

数组 words[wordIndex] 元素位操作

  在找到数组下标之后,要进行元素的 bit 操作。「bit 默认为 0 」 这里引入了 “ 或 ” 运算,运算规则如下:

|=:两个二进制对应位都为0时,结果等于0,否则结果等于1;

  由于 Long 类型是 8 字节,64 位,所以这里要进行移位运算,找到属于 bitIndex 的 bit ,将其置 1 。源码为:

words[wordIndex] |= (1L << bitIndex);

  即翻译成可读性较强的是下面的样子:

words[wordIndex] = [wordIndex] |1 << (bitIndex % 64)

  完成之后,数据标识已写入。
  总体来讲,BitSet 实现及写入逻辑并不困难。 下面将给出 Go 版的 Java 原生 BitSet 写法。

Go 重写 Java 原生包 BitSet

  了解了 Java 原生 BitSet 包的具体实现,换种语言解释也是十分简单,Go 版本如下:

//将 string 转为 bit 桶
sDec, err := base64.StdEncoding.DecodeString(group.Buckets)
if err != nil {
   logger.Info("ab process base64.StdEncoding.DecodeString(bucket) fail:%s", err.Error())
   break
}
dst := make([]bool, experiment.Partitions)
for i, _ := range dst {
   //防止解析之后不够partitions大小
   bitIndex := i / 8
   if bitIndex < len(sDec) {
      dst[i] = (sDec[bitIndex] & (1 << (i % 8))) != 0
   }
}

Q&A

1、在计算数组元素位操作时,为什么 " 1L << bitIndex " 等价于 " 1 << (bitIndex % 64)" 呢?

是因为 Long 类型数据一共有 64 位,进行移位运算的话,有可能有效位全移出去了,所以需要模上长度。
也可以理解为,每个元素长度是 64,要算当前 bitIndex 所属的位置,取模是一种很好的映射关系。

2、Go 里面没有实现的 BitSet 吗?

Go 没有原生实现 BitSet ,有许多开源包,但开源包各有各的策略,不能保证和 Java 原生 BitSet 集合的存储关系一致!

附录

开发语言,不过是原理的诠释手段之一…

以上是关于为啥Java中的BitSet使用long数组做内部存储,而不使用int数组...的主要内容,如果未能解决你的问题,请参考以下文章

BitSet 的使用

为啥带有 unsigned long long 参数的 std::bitset 构造函数未标记为显式?

一分钟轻松掌握 !Java 高级数据结构 -- 原生 BitSet 源码刨析

一分钟轻松掌握 !Java 高级数据结构 -- 原生 BitSet 源码刨析

一分钟轻松掌握 !Java 高级数据结构 -- 原生 BitSet 源码刨析

Java Bitset