我发现有Set
的实现使用哈希(具有所有有用的后果,如contains()
等的O(1)),据称比{{更有效1}}在每个方面:
http://ontopia.wordpress.com/2009/09/23/a-faster-and-more-compact-set/
http://alias-i.com/lingpipe/docs/api/com/aliasi/util/CompactHashSet.html
当我需要java.util.HashSet
支持java.util.HashSet
时,完全退出java.util.Set
是不是一个好主意?
答案 0 :(得分:9)
让我们开始一场基准游戏。
基准是基于原始文章的基准,但使用现代工具。
package tests;
import com.carrotsearch.hppc.ObjectOpenHashSet;
import com.carrotsearch.hppc.cursors.ObjectCursor;
import com.google.common.collect.GuavaCompactHashSet;
import net.ontopia.utils.CompactHashSet;
import net.openhft.koloboke.collect.set.hash.HashObjSet;
import net.openhft.koloboke.collect.set.hash.HashObjSets;
import org.openjdk.jmh.annotations.*;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import static java.util.Arrays.stream;
import static org.openjdk.jol.info.GraphLayout.parseInstance;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@OperationsPerInvocation(TestHashSet.TIMES)
@Threads(1)
@Fork(1)
@State(Scope.Thread)
public class TestHashSet {
public static final int TIMES = 1000000;
private static final int MAX = 5000000;
private static long ELEMENTS_SIZE;
static Long[] add = new Long[TIMES], lookup = new Long[TIMES], remove = new Long[TIMES];
static {
for (int ix = 0; ix < TIMES; ix++)
add[ix] = new Long(Math.round(Math.random() * MAX));
ELEMENTS_SIZE = stream(add).distinct().count() * parseInstance(add[0]).totalSize();
for (int ix = 0; ix < TIMES; ix++)
lookup[ix] = new Long(Math.round(Math.random() * MAX));
for (int ix = 0; ix < TIMES; ix++)
remove[ix] = new Long(Math.round(Math.random() * MAX));
}
@Benchmark
public int hashSet() {
Set<Long> set = new HashSet<Long>();
for (Long o : add) {
set.add(o);
}
int r = 0;
for (Long o : lookup) {
r ^= set.contains(o) ? 1 : 0;
}
for (Long o : set) {
r += o.intValue();
}
for (Long o : remove) {
set.remove(o);
}
return r + set.size();
}
@Benchmark
public int compactHashSet() {
Set<Long> set = new CompactHashSet<Long>();
for (Long o : add) {
set.add(o);
}
int r = 0;
for (Long o : lookup) {
r ^= set.contains(o) ? 1 : 0;
}
for (Long o : set) {
r += o.intValue();
}
for (Long o : remove) {
set.remove(o);
}
return r + set.size();
}
@Benchmark
public int hppcSet() {
ObjectOpenHashSet<Long> set = new ObjectOpenHashSet<Long>();
for (Long o : add) {
set.add(o);
}
int r = 0;
for (Long o : lookup) {
r ^= set.contains(o) ? 1 : 0;
}
for (ObjectCursor<Long> cur : set) {
r += cur.value.intValue();
}
for (Long o : remove) {
set.remove(o);
}
return r + set.size();
}
@Benchmark
public int kolobokeSet() {
Set<Long> set = HashObjSets.newMutableSet();
for (Long o : add) {
set.add(o);
}
int r = 0;
for (Long o : lookup) {
r ^= set.contains(o) ? 1 : 0;
}
for (Long o : set) {
r += o.intValue();
}
for (Long o : remove) {
set.remove(o);
}
return r + set.size();
}
@Benchmark
public int guavaCompactHashSet() {
// fair comparison -- growing table
Set<Long> set = new GuavaCompactHashSet<>(10);
for (Long o : add) {
set.add(o);
}
int r = 0;
for (Long o : lookup) {
r ^= set.contains(o) ? 1 : 0;
}
for (Long o : set) {
r += o.intValue();
}
for (Long o : remove) {
set.remove(o);
}
return r + set.size();
}
public static void main(String[] argv) {
HashSet hashSet = new HashSet();
test("HashSet", hashSet, hashSet::add);
CompactHashSet compactHashSet = new CompactHashSet();
test("CompactHashSet", compactHashSet, compactHashSet::add);
HashObjSet<Object> kolobokeSet = HashObjSets.newMutableSet();
test("KolobokeSet", kolobokeSet, kolobokeSet::add);
ObjectOpenHashSet hppcSet = new ObjectOpenHashSet();
test("HPPC set", hppcSet, hppcSet::add);
GuavaCompactHashSet guavaCompactHashSet = new GuavaCompactHashSet(10);
test("GuavaCompactHashSet", guavaCompactHashSet, guavaCompactHashSet::add);
}
public static void test(String name, Object set, Consumer setAdd) {
for (Long o : add) {
setAdd.accept(o);
}
System.out.printf("%s: %.1f bytes per element\n", name,
((parseInstance(set).totalSize() - ELEMENTS_SIZE) * 1.0 / TIMES));
}
}
结果:
Set implementation Speed Memory footprint
Score Units +UCOops -UseCompressedOops
CompactHashSet 828 ns/op 8.4 16.8 bytes/elem
HashSet 676 ns/op 37.4 60.3 bytes/elem
HPPC Set 853 ns/op 10.5 18.9 bytes/elem
Koloboke Set 587 ns/op 8.4 16.8 bytes/elem
GuavaCompactHashSet 874 ns/op 25.9 37.4 bytes/elem
显示CompactHashSet
比旧版HashSet
更慢,尽管它使用的内存要少得多。
答案 1 :(得分:3)
取决于。
您是在处理非常大的集合以及许多插入或读取操作吗?这项新的实施将一百万次运营的时间缩短了一半。这是一个很大的进步,但如果你只做了几千次操作或打了十几次,那么这很快就变成了微观优化。
显示的测试也是在集合中插入Long
。如果您在集合中存储其他内容,则运行时和内存使用的性能可能会发生变化。
如果您的用例可以从统计上显着的方式改变中获益,那么请使用它。
答案 2 :(得分:3)
选项1:不关心。如果查看java HashSet实现,您会发现它只是在内部使用HashMap:
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
static final long serialVersionUID = -5024744406713321676L;
private transient HashMap<E,Object> map;
....
这是一个快速实现,但是,每个set条目都引用了一个不需要的值。因此内存消耗。我的第一个选择是“不关心”,因为我希望将来有人会在JDK中提供改进的HashSet。软件工程师应该始终抱有希望和积极的态度:)
在正常的程序逻辑中,我总是尽可能地坚持所提供的标准并使用可用的标准。这避免了每个程序员使用自己的“最喜欢的Set实现”的效果,或者更糟糕的是,做了一个冗长的研究,实际上最好使用的HashSet实现是什么;)
Oracle是否为穷人HashMap提供了一个开放的漏洞票?找不到一个....
选项2:关怀。如果您没有业务逻辑价值,但在某些技术中间件代码中,那么性能可能很重要。然后有各种选择。 Google Guava中的CompactHashMap就是其中之一。另一个不错的库是High Performance Primitive Collections。在HPPC中,您还可以找到每种基本类型的集合。我想你也会找到适合你特定目的的其他东西。并非每个HashMap替换都可能具有与原始HashMap完全相同的语义。
所以,我个人永远不会“默认”替换java.util.HashMap。