我正在尝试实现一个BloomFilter,并遇到了一些关于BitSet的讨论。 Lucene OpenBitSet声称它几乎在所有操作中都比Java BitSet实现更快。
我试着查看两个实现的代码。
Java BitSet代码
在我看来,这两个类都使用'long'数组来存储这些位。各个位映射到特定的数组索引和存储在索引处的'long'值中的位位置。
是什么原因,那么OpenBitSet实现在性能方面要好得多?导致速度提高的代码差异在哪里?
答案 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
令人惊讶,不是吗?我们可以从结果中学到什么?
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
越快,当比特均匀分布时,BitSet
和OpenBitSet
的性能变得相等,理论证实。因此,对于特定的非均匀设置位分布,经典BitSet
对于组操作来说更快。关于OpenBitSet
中非常快速的群组操作的声明是 false 。
这个答案和基准并不打算表明OpenBitSet
不好或作者是骗子。实际上,根据他们的基准测试机器(AMD Opteron和Pentium 4)和Java版本(1.5),很容易相信早期 BitSet
的优化程度较低,Hotspot编译器不是&# 39;非常聪明,popcnt
指令不存在,然后OpenBitSet
是个好主意,性能更高。此外,BitSet
没有公开其内部字数组,因此无法创建自定义的细粒度同步bitset或灵活的序列化,这是Lucene所需要的。因此对于Lucene来说,它仍然是一个合理的选择,而对于典型的用户来说,使用标准BitSet
会更好,这更快(在某些情况下,通常不是)并且属于标准库。时间变化,旧的性能结果发生变化,因此始终对您的特定情况进行基准测试和验证,可能对其中一些情况(例如,不是基准测试迭代器或不同的设置填充因子)OpenBitSet
会更快。
答案 1 :(得分:0)
1.5x
,OpenBitSet承诺3x
到cardinality
的速度更快,
iteration
和get
。它还可以处理更大的基数集(最高64 * 2 ** 32-1)。Lucene 3.0
中,整个IndexReader类树都是
重写不要像锁定,重新打开和参考一样混乱
计数。您实际上是针对大小为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倍。
资源链接:
OpenBitSet的目标是
fastest implementation
可能的, 和maximum code reuse
。额外的安全性和封装可能永远是 建立在顶部,但如果内置,成本永远不会被删除 (因此人们重新实施自己的版本才能获得 更好的表现)
因此,如果您想要一个“安全”,完全封装(且速度较慢且有限)的BitSet类,请使用java.util.BitSet
。
从现有的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
答案 2 :(得分:0)
免责声明:这个答案是在没有任何关于效率如何的研究的情况下完成的 是有问题的bitset实现,这更像是一般 关于算法设计的智慧。
如文档中所述,某些特定操作的实施速度更快。OpenBitSet
那么,使用标准Java BitSet
更好吗?可能是的,但不是因为速度,而是因为开放性。为什么呢?
当您设计算法时,要做出以下决定:您是希望它在大多数情况下同样执行还是在某些特定情况下表现更好,但在其他情况下可能会丢失?
我认为,java.util.BitSet
的作者采取了第一条路线。 Lucene实现很可能对操作更快,对于他们的问题域更重要。但是他们也将实现打开,这样你就可以覆盖行为以优化对你很重要的案例。
那么OpenBitSet
中打开究竟是什么?文档告诉和来源确认实现基本上公开位的底层表示到子类。这既好又坏:容易改变行为,也容易拍自己的脚。也许这就是为什么(只是一个疯狂的猜测!)在较新版本的Lucene中他们采取了其他路径:删除OpenBitSet
以支持另一个BitSet
实现,该实现尚未打开,但不会暴露数据结构。实现(FixedBitSet
,SparseFixedBitSet
)完全负责自己的数据结构。
参考文献:
https://issues.apache.org/jira/browse/LUCENE-6010
http://lucene.apache.org/core/6_0_0/core/org/apache/lucene/util/BitSet.html