【问题标题】:Should I dump java.util.HashSet in favor of CompactHashSet? [closed]我应该转储 java.util.HashSet 以支持 CompactHashSet 吗? [关闭]
【发布时间】:2014-10-14 16:14:38
【问题描述】:

我发现有一个Set 的实现使用散列(具有所有有用的结果,例如contains() 的O(1) 等),据称在各个方面都比java.util.HashSet 更有效:

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.Set 的地方完全放弃使用java.util.HashSet 转而使用com.aliasi.util.CompactHashSet 是否是个好主意?

【问题讨论】:

  • HashSet 运行良好时,为什么还要向您的项目添加另一个 JAR 依赖项?当然,除非您正在开发低延迟应用程序并且您知道您有性能或堆内存问题
  • 您在使用 HashSet 时是否存在性能问题?如果是这样,做你自己的基准测试,看看它有什么好处。然后你可以决定是否需要切换。
  • @Mohammad:因为使用它的人少得多,所以它更容易包含可能尚未发现的错误。
  • 有趣的是Guava also has a CompactHashSet,如果您确实想包含一个额外的 JAR,它提供更多的功能并将被 Java 社区更广泛地使用
  • 另外,如果您阅读第二个链接,CompactHashSet 与 HashSet 兼容。除其他事项外,不能使用 null,并且您可能会在某些不会抛出 HashSet 的情况下获得 ClassCastExceptions。

标签: java performance collections big-o hashset


【解决方案1】:

让我们开始一个小基准测试游戏。

基准基于原始文章中的基准,但使用现代工具。

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 还要慢,尽管它使用的内存要少得多。

【讨论】:

  • CompactHashSet 以相同方式使用链接条目时,我不明白HashSet 怎么会比HashSet 小近4 倍。
  • 我明白了...这是一个不同的CompactHashSet,我指的是来自 Google 的那个。
  • @maaartinus 更新了答案。老实说,我不明白为什么java.util.HashSet 被广泛批评为如此“懒惰”HashMap-delegating 实现对特别优化的实现表现如此之好。也许基准测试有错误?
  • 1.有一些取舍。您可以使用更密集的HashMap 并使其更小更慢。 2. 你的数据是完全随机的,这可能有利于某些实现。 3. 你的“查找”几乎找不到任何东西,这也可能造成偏见。 4.“删除”同理。 5. 实际上,需要许多不同的基准来获得完整的答案(但是我们可以用这么多数字做什么)。
  • @cruftex HashMap.Entry 对于 HashSet 来说似乎很浪费,但在 32 位 JVM 或使用“CompressedOops”时,无用字段 value 由于对齐是免费的。 Guava 的 CompactHashSet 忽略了 valuehash,后者在 HM 中用于提高速度(避免键比较)。
【解决方案2】:

视情况而定。

您是否正在处理非常大的集合和许多插入或读取操作?这种新的实施将一百万次操作的时间缩短了一半。这是一个很大的改进,但如果你只进行几千次或十几次操作,那么这很快就会变成微优化。

显示的测试还将Long 插入到集合中。如果您在集合中存储其他内容,则运行时和内存使用的性能可能会发生变化。

如果您的用例可证明以显着统计方式从更改中受益,请使用它。

【讨论】:

    【解决方案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;
    ....
    

    这是一个快速的实现,但是,每个集合条目都有一个对值的引用,这不是必需的。因此内存消耗。我的第一个选择是“不关心”,因为我希望将来某个时候有人会在 JDK 中提供改进的 HashSet。软件工程师应该永远抱有希望和积极的态度:)

    在正常的程序逻辑中,我总是尽可能地坚持提供的标准并使用可用的标准。这避免了每个程序员使用自己的“最喜欢的 Set 实现”的效果,或者更糟糕的是,做一个冗长的研究什么是实际最好的 HashSet 实现;)

    Oracle 是否有针对可怜的 HashMap 的开放错误票?找不到……

    选项 2:关心。如果您不在业务逻辑价值上而是在一些技术中间件代码中,那么性能可能很重要。然后有各种选择。 Google Guava 中的 CompactHashMap 就是其中之一。另一个不错的库是High Performance Primitive Collections。在 HPPC 中,您还可以找到每种原始类型的集合。我认为您还会找到其他适合您特定目的的东西。并非每个 HashMap 替换都可能具有与原始 HashMap 完全相同的语义。

    所以,我个人永远不会“默认”替换 java.util.HashMap。

    猜你喜欢
    • 2010-09-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-08-05
    • 2011-09-01
    • 2023-01-06
    • 2010-09-15
    相关资源
    最近更新 更多