【问题标题】:Java JDK BitSet vs Lucene OpenBitSetJava JDK BitSet 与 Lucene OpenBitSet
【发布时间】:2016-09-01 23:30:37
【问题描述】:

我试图实现一个 BloomFilter 并遇到了一些关于 BitSets 的讨论。 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 实现在性能方面要好得多的原因是什么?导致速度提高的代码差异在哪里?

【问题讨论】:

    标签: java performance lucene bitset


    【解决方案1】:

    好的,你就是这样处理这些事情的。

    当有人声称他的实现速度提高了 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
    

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

    • Get 和 set(包括快速版本)在性能方面是相同的。他们的结果处于相同的误差范围内,如果没有适当的纳米基准测试,很难分辨出任何差异,因此就在典型应用程序实现中使用 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] 的范围内执行和/或/基数操作,因此我们可以比较特定情况,当 set 只有第一个设置了 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
      

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


    总结

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

    【讨论】:

      【解决方案2】:

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

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

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

      我认为java.util.BitSet 的作者选择了第一条路线。 Lucene 实现很可能对于操作来说更快,这对于他们的问题域更重要。但他们也让实现开放,以便您可以覆盖行为以针对对您很重要的情况进行优化。

      那么,OpenBitSet 中的 open 到底是什么?文档告诉和消息来源确认,该实现基本上位的底层表示形式暴露给子类。这有好有坏:很容易改变行为,但也很容易开枪打自己的脚。也许这就是为什么(只是一个疯狂的猜测!)在较新版本的 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

      【讨论】:

        【解决方案3】:

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

        1. OpenBitSet 承诺 1.5x3x 对于 cardinality 的速度更快, iterationget。它还可以处理更大的基数集(最多 64 * 2**32-1)。
        2. 当 BitSet 对于没有外部的多线程使用不安全时 同步,OpenBitSet 允许有效地实现 替代序列化或交换格式。
        3. 对于 OpenBitSet,始终可以构建额外的安全性和封装性 在顶部,但在 BitSet 中却不是。
        4. OpenBitSet 允许直接访问存储 位,但在 BitSet 中,它实现了一个位向量,其增长为 需要。
        5. IndexReader 和 SegmentMerger 更具定制性和可插入性 开放比特集。在Lucene 3.0 中,整个 IndexReader 类树是 重写为不会弄乱锁定,重新打开和引用 计数。
        6. 在 Solr 中,如果您有一组那么小的文档,它会最 可能使用 HasDocSet 而不是 BitDocSet 建模。

        例如,

        您实际上是在测试大小为 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.
        

        无论如何,如果只设置一个位,那么有很多 计算交叉点大小的更快方法。

        如果你想看看 OpenBitSet 相对于 BitSet 的表现,那就去吧 通过此链接: 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:

        OpenBitSet的目标fastest implementation可能的, 和 maximum code reuse。额外的安全和封装可能总是 建立在顶部,但如果它是内置的,则永远无法消除成本 (因此人们重新实现他们自己的版本以获得 更好的性能)

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


        OpenBitSet 是如何工作的?

        从现有的 long[] 构造一个 OpenBitSet。前 64 位 在 long[0] 中,位索引 0 在最低有效位,位 索引 63 最显着。给定一个位索引,单词 包含它是 long[index/64],并且它位于位数 index%64 在那个词中。 numWords 是数组中元素的数量 包含设置位(非零长)。 numWords 应该是 = 的任何现有单词 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?

        【讨论】:

        • 你的回答都是关于一般信息的,它没有回答“为什么 X 比 Y 快”的问题
        • @qwwdfsad 在基数、迭代和获取本节上更快。如果你通读它,你可以很容易地知道它为什么更快。我也给出了一些关键点作为更新部分。
        • 好的,我正在阅读基数方法:它们实际上是相同的。为什么其中一个更快?
        • @SkyWalker 为什么不直接裁剪确切的答案?我不明白这篇“维基百科文章”如何帮助任何来到这里的人知道 OP 问题的答案..
        • @tair 我已经在第一部分给出了关键点。然后是细节。希望它会有所帮助
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-02-11
        • 2010-09-17
        • 2012-12-11
        • 1970-01-01
        • 1970-01-01
        • 2011-04-26
        相关资源
        最近更新 更多