好吧,这就是你处理这些事情的方式。
当有人声称他的实现速度快2-3倍,使用诸如“最大代码重用”,“没有额外的安全性”等常用短语并且没有提供任何真正的基准时,您应该在脑海中举起危险信号。事实上,邮件列表/文档中的所有基准测试都没有源代码,并且(根据结果)是手动编写的(因此可能违反了基准测试规则),而不是使用JMH。
在挥手解释为什么某些东西比其他东西更快之前,让我们写一个基准,看看它是否真的更快,然后再做出任何陈述。基准代码在这里:它只是测试填充因子为50%的大小为1024和1024 * 1024(~1kk)的集合的所有基本操作。测试在英特尔酷睿 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(包括快速版本)在性能方面是相等的。它们的结果位于相同的误差范围内,如果没有适当的纳米基准标记,很难分辨出任何差异,因此在典型应用程序实现中使用位集没有任何区别,如果分支无关紧要,则再有一个区别。因此,关于获取/设置更好的性能的陈述是错误的。UPD:获取方法的纳米基准也没有显示出任何差异,结果在这里。
OpenBitSet
- 的基数可以计算得更快(对于 1k 和 1kk 大小,大约是 3 倍),因此关于“超快速基数”的陈述是错误的。但是,如果没有实际答案,为什么性能会有所不同,数字就毫无意义,所以让我们来挖掘一下。在单词中计算位使用是热点固有的。这意味着整个方法将被编译成单个指令(对于好奇的方法,它将是x86 )。虽然使用黑客喜悦中的技巧手动滚动位计数(请参阅)。难怪为什么经典版本现在更快。
BitSet
BitSet
Long#bitCount
bitCount
popcnt
OpenBitSet
org.apache.lucene.util.BitUtil#pop_array
-
像和/或这样的组集方法都是相同的,所以这里没有性能优势。但有趣的是:实现跟踪单词的最大索引,其中至少设置了一个位,并且仅在[0,maxIndex]的范围内执行和/或/基数操作,因此我们可以比较特定情况,当set仅设置了第一个1/10/50%位而其余部分没有(给定部分的填充因子为50%)。然后性能应该有所不同,同时保持相同。让我们验证一下(基准代码):BitSet
BitSet
OpenBitSet
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
总结
这个答案和基准并不打算表明这是坏事或作者是骗子。事实上,根据他们的基准机器(AMD皓龙和奔腾4)和Java版本(1.5),很容易相信早期的优化程度较低,Hotspot编译器不是很聪明,指令不存在,然后是一个好主意,性能要高得多。此外,不会公开其内部单词数组,因此不可能创建自定义的细粒度同步位集或灵活的序列化,而这正是Lucene所需要的。因此,对于Lucene来说,这仍然是一个合理的选择,而对于典型用户来说,最好使用标准,这更快(在某些情况下,不是一般情况下)并且属于标准库。时间变化,旧的性能结果变化,所以总是对特定案例进行基准测试和验证,也许对于其中一些案例(例如,没有基准测试迭代器或不同的设置填充因子)会更快。OpenBitSet
BitSet
popcnt
OpenBitSet
BitSet
BitSet
OpenBitSet