Java JDK BitSet与Lucene OpenBitSet

时间:2016-05-06 19:28:03

标签: java performance lucene bitset

我正在尝试实现一个BloomFilter,并遇到了一些关于BitSet的讨论。 Lucene OpenBitSet声称它几乎在所有操作中都比Java BitSet实现更快。

http://grepcode.com/file/repo1.maven.org/maven2/org.apache.lucene/lucene-core/4.10.4/org/apache/lucene/util/OpenBitSet.java#OpenBitSet

我试着查看两个实现的代码。

Java BitSet代码

http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/8u40-b25/java/util/BitSet.java#BitSet

在我看来,这两个类都使用'long'数组来存储这些位。各个位映射到特定的数组索引和存储在索引处的'long'值中的位位置。

是什么原因,那么OpenBitSet实现在性能方面要好得多?导致速度提高的代码差异在哪里?

3 个答案:

答案 0 :(得分:18)

好的,那就是你如何处理这些事情。

当有人声称他的实施速度比常用短语快2-3倍,例如"最大代码重用","没有额外的安全性"等等,并没有提供任何真正的基准,你应该提出你头上的红旗。实际上,他们的邮件列表/文档中的所有基准都没有源代码并且手写(根据结果)(可能违反了benchmarking rules)而不是使用JMH。

在挥手为什么比某些东西更快的东西之前,让我们写一个基准,看看它在发表任何陈述之前是否真的更快。 基准代码是here:它只测试1024和1024 * 1024(~1kk)大小的所有基本操作,填充因子为50%。测试在Intel Core i7-4870HQ CPU @ 2.50GHz上运行。分数是吞吐量,越高越好。

整个基准测试如下:

  @Benchmark
  public boolean getClassic(BitSetState state) {
      return state.bitSet.get(state.nextIndex);
  }

  @Benchmark
  public boolean getOpen(BitSetState state) {
      return state.openBitSet.get(state.nextIndex);
  }

  @Benchmark
  public boolean getOpenFast(BitSetState state) {
      return state.openBitSet.fastGet(state.nextIndex);
  }

好的,让我们看看结果:

Benchmark                           (setSize)   Mode  Cnt    Score    Error   Units
BitSetBenchmark.andClassic               1024  thrpt    5  109.541 ± 46.361  ops/us
BitSetBenchmark.andOpen                  1024  thrpt    5  111.039 ±  9.648  ops/us
BitSetBenchmark.cardinalityClassic       1024  thrpt    5   93.509 ± 10.943  ops/us
BitSetBenchmark.cardinalityOpen          1024  thrpt    5   29.216 ±  4.824  ops/us
BitSetBenchmark.getClassic               1024  thrpt    5  291.944 ± 46.907  ops/us
BitSetBenchmark.getOpen                  1024  thrpt    5  245.023 ± 75.144  ops/us
BitSetBenchmark.getOpenFast              1024  thrpt    5  228.563 ± 91.933  ops/us
BitSetBenchmark.orClassic                1024  thrpt    5  121.070 ± 12.220  ops/us
BitSetBenchmark.orOpen                   1024  thrpt    5  107.612 ± 16.579  ops/us
BitSetBenchmark.setClassic               1024  thrpt    5  527.291 ± 26.895  ops/us
BitSetBenchmark.setNextClassic           1024  thrpt    5  592.465 ± 34.926  ops/us
BitSetBenchmark.setNextOpen              1024  thrpt    5  575.186 ± 33.459  ops/us
BitSetBenchmark.setOpen                  1024  thrpt    5  527.568 ± 46.240  ops/us
BitSetBenchmark.setOpenFast              1024  thrpt    5  522.131 ± 54.856  ops/us



Benchmark                           (setSize)   Mode  Cnt    Score    Error   Units
BitSetBenchmark.andClassic            1232896  thrpt    5    0.111 ±  0.009  ops/us
BitSetBenchmark.andOpen               1232896  thrpt    5    0.131 ±  0.010  ops/us
BitSetBenchmark.cardinalityClassic    1232896  thrpt    5    0.174 ±  0.012  ops/us
BitSetBenchmark.cardinalityOpen       1232896  thrpt    5    0.049 ±  0.004  ops/us
BitSetBenchmark.getClassic            1232896  thrpt    5  298.027 ± 40.317  ops/us
BitSetBenchmark.getOpen               1232896  thrpt    5  243.472 ± 87.491  ops/us
BitSetBenchmark.getOpenFast           1232896  thrpt    5  248.743 ± 79.071  ops/us
BitSetBenchmark.orClassic             1232896  thrpt    5    0.135 ±  0.017  ops/us
BitSetBenchmark.orOpen                1232896  thrpt    5    0.131 ±  0.021  ops/us
BitSetBenchmark.setClassic            1232896  thrpt    5  525.137 ± 11.849  ops/us
BitSetBenchmark.setNextClassic        1232896  thrpt    5  597.890 ± 51.158  ops/us
BitSetBenchmark.setNextOpen           1232896  thrpt    5  485.154 ± 63.016  ops/us
BitSetBenchmark.setOpen               1232896  thrpt    5  524.989 ± 27.977  ops/us
BitSetBenchmark.setOpenFast           1232896  thrpt    5  532.943 ± 74.671  ops/us

令人惊讶,不是吗?我们可以从结果中学到什么?

  • 获取和设置(包括快速版本)在性能方面是相同的。它们的结果存在于相同的误差范围内,如果没有适当的纳米标记,很难说出任何差异,因此在典型应用实现中使用bitset方面并没有任何区别,如果分支没有做出更多的分析。无所谓。因此,关于OpenBitSet获取/设置更好性能的声明是 false 。 UPD:get方法的nanobenchmark也没有显示任何差异,结果是here
  • BitSet的基数可以更快地计算(对于1k和1kk大小都可以计算~3倍),因此关于"超快基数" false 。但是如果没有实际答案,为什么性能不同,数字就毫无意义,所以让我们稍微挖掘一下。要计算单词BitSet中的位,请使用Long#bitCount,即热点intrinsic。这意味着整个bitCount方法将被编译成单指令(对于好奇的方法,它将是x86 popcnt)。虽然OpenBitSet使用来自Hacker's Delight的技巧进行手动滚动计数(请参阅org.apache.lucene.util.BitUtil#pop_array)。难怪为什么经典版本现在更快。
  • 像和/或组的集合方法都是相同的,所以这里没有性能胜利。但有趣的是:BitSet实现跟踪字的最大索引,其中至少有一个位被设置并且仅在[0,maxIndex]的边界内执行和/或/基数操作,因此我们可以比较具体情况,当设置有只设置了第一个1/10 / 50%位而其余部分没有设置(给定部分的填充系数相同,为50%)。然后BitSet表现应有所不同,而OpenBitSet保持不变。让我们验证(benchmark code):

    Benchmark                   (fillFactor)  (setSize)   Mode  Cnt   Score    Error   Units
    BitSetBenchmark.andClassic          0.01    1232896  thrpt    5  32.036 ±  1.320  ops/us
    BitSetBenchmark.andClassic           0.1    1232896  thrpt    5   3.824 ±  0.896  ops/us
    BitSetBenchmark.andClassic           0.5    1232896  thrpt    5   0.330 ±  0.027  ops/us
    BitSetBenchmark.andClassic             1    1232896  thrpt    5   0.140 ±  0.017  ops/us
    BitSetBenchmark.andOpen             0.01    1232896  thrpt    5   0.142 ±  0.008  ops/us
    BitSetBenchmark.andOpen              0.1    1232896  thrpt    5   0.128 ±  0.015  ops/us
    BitSetBenchmark.andOpen              0.5    1232896  thrpt    5   0.112 ±  0.015  ops/us
    BitSetBenchmark.andOpen                1    1232896  thrpt    5   0.132 ±  0.018  ops/us
    BitSetBenchmark.orClassic           0.01    1232896  thrpt    5  27.826 ± 13.312  ops/us
    BitSetBenchmark.orClassic            0.1    1232896  thrpt    5   3.727 ±  1.161  ops/us
    BitSetBenchmark.orClassic            0.5    1232896  thrpt    5   0.342 ±  0.022  ops/us
    BitSetBenchmark.orClassic              1    1232896  thrpt    5   0.133 ±  0.021  ops/us
    BitSetBenchmark.orOpen              0.01    1232896  thrpt    5   0.133 ±  0.009  ops/us
    BitSetBenchmark.orOpen               0.1    1232896  thrpt    5   0.118 ±  0.007  ops/us
    BitSetBenchmark.orOpen               0.5    1232896  thrpt    5   0.127 ±  0.018  ops/us
    BitSetBenchmark.orOpen                 1    1232896  thrpt    5   0.148 ±  0.023  ops/us
    

集合的下半部分被填充,BitSet越快,当比特均匀分布时,BitSetOpenBitSet的性能变得相等,理论证实。因此,对于特定的非均匀设置位分布,经典BitSet对于组操作来说更快。关于OpenBitSet中非常快速的群组操作的声明是 false

摘要

这个答案和基准并不打算表明OpenBitSet不好或作者是骗子。实际上,根据他们的基准测试机器(AMD Opteron和Pentium 4)和Java版本(1.5),很容易相信早期 BitSet的优化程度较低,Hotspot编译器不是&# 39;非常聪明,popcnt指令不存在,然后OpenBitSet是个好主意,性能更高。此外,BitSet没有公开其内部字数组,因此无法创建自定义的细粒度同步bitset或灵活的序列化,这是Lucene所需要的。因此对于Lucene来说,它仍然是一个合理的选择,而对于典型的用户来说,使用标准BitSet会更好,这更快(在某些情况下,通常不是)并且属于标准库。时间变化,旧的性能结果发生变化,因此始终对您的特定情况进行基准测试和验证,可能对其中一些情况(例如,不是基准测试迭代器或不同的设置填充因子)OpenBitSet会更快。

答案 1 :(得分:0)

为什么OpenBitSet在性能方面比BitSet更好?举一些相关的例子。

  1. 对于1.5x,OpenBitSet承诺3xcardinality的速度更快, iterationget。它还可以处理更大的基数集(最高64 * 2 ** 32-1)。
  2. 当没有外部的多线程使用时,BitSet不安全 同步,OpenBitSet允许有效地实现 备用序列化或交换格式。
  3. 对于OpenBitSet,可以始终构建额外的安全性和封装 在顶部,但在BitSet中它不是。
  4. OpenBitSet允许直接访问存储的单词数组 位,但在BitSet中,它实现了一个增长为的位向量 需要的。
  5. IndexReader和SegmentMerger更加个性化和可插入 OpenBitSet。在Lucene 3.0中,整个IndexReader类树都是 重写不要像锁定,重新打开和参考一样混乱 计数。
  6. 在Solr中,如果你有一套很小的文件,那就最好了 可能使用HasDocSet而不是BitDocSet建模。
  7. 例如,

    您实际上是针对大小为5000的小组来测试大小为500,000的小组。

    BitSet跟踪您设置的最大位(即5000)和 实际上并不计算交集或populationCount 除此之外。 OpenBitSet没有(它试图做到最小 必要的,尽可能快地完成所有事情。)

    So if you changed the single bit you set from 5000 to 499,999, you
    should see very different results.
    

    在任何情况下,如果只设置一个位,那么就有很多 更快的计算交叉点大小的方法。

      

    如果你想通过BitSet看到OpenBitSet的性能,那就去吧   通过这个链接:   http://lucene.apache.org/core/3_0_3/api/core/org/apache/lucene/util/OpenBitSet.html

    相关链接:Benchmarking results of mysql, lucene and sphinx

      

    在我看来,这两个类都使用'long'数组来存储这些位。   是什么原因,然后OpenBitSet实现是远的   在性能方面更好?

    实际上,性能取决于java.util.BitSet和OpenBitSet设置的算法。 OpenBitSet在大多数操作中比java.util.BitSet快,在计算集合的基数和集合操作的结果时,更快。它还可以处理更大的基数集(最高64 * 2 ** 32-1) 对于基数,迭代和获取,OpenBitSet的速度提高了1.5倍到3倍。

    资源链接:

    1. OpenBitSet Performance
    2. Behaviour of BitSet:
    3.   

      OpenBitSet的目标fastest implementation可能的,   和       maximum code reuse。额外的安全性和封装可能永远是       建立在顶部,但如果内置,成本永远不会被删除       (因此人们重新实施自己的版本才能获得       更好的表现)

      因此,如果您想要一个“安全”,完全封装(且速度较慢且有限)的BitSet类,请使用java.util.BitSet

      OpenBitSet如何工作?

        

      从现有的long []构造一个OpenBitSet。前64位   在long [0]中,最低有效位的位索引为0,位为bit   指数63最重要。给出一点索引,这个词   包含它的是[index / 64],它是位数索引%64   在那个词里面。 numWords是数组中元素的数量   包含设置位(非零长)。 numWords应该是< =   bits.length,以及位于> =的数组中的任何现有单词   numWords应为零。

      资源链接:

      OpenBitSet示例:http://www.massapi.com/class/op/OpenBitSet.html

      资源链接:

      1. Java BitSet Example
      2. There is a casestudy which shows how much effective and how they improve in lucene's OpenBitSet?

答案 2 :(得分:0)

  

免责声明:这个答案是在没有任何关于效率如何的研究的情况下完成的   是有问题的bitset实现,这更像是一般   关于算法设计的智慧。

如文档中所述,某些特定操作的实施速度更快。OpenBitSet那么,使用标准Java BitSet更好吗?可能是的,但不是因为速度,而是因为开放性。为什么呢?

当您设计算法时,要做出以下决定:您是希望它在大多数情况下同样执行还是在某些特定情况下表现更好,但在其他情况下可能会丢失?

我认为,java.util.BitSet的作者采取了第一条路线。 Lucene实现很可能对操作更快,对于他们的问题域更重要。但是他们也将实现打开,这样你就可以覆盖行为以优化对你很重要的案例。

那么OpenBitSet打开究竟是什么?文档告诉和来源确认实现基本上公开位的底层表示到子类。这既好又坏:容易改变行为,也容易拍自己的脚。也许这就是为什么(只是一个疯狂的猜测!)在较新版本的Lucene中他们采取了其他路径:删除OpenBitSet以支持另一个BitSet实现,该实现尚未打开,但不会暴露数据结构。实现(FixedBitSetSparseFixedBitSet)完全负责自己的数据结构。

参考文献:

https://issues.apache.org/jira/browse/LUCENE-6010

http://lucene.apache.org/core/6_0_0/core/org/apache/lucene/util/BitSet.html