【问题标题】:Is remove() faster than get() in HashMap?HashMap 中的 remove() 是否比 get() 快?
【发布时间】:2016-01-20 12:31:16
【问题描述】:

我已经为getremove 编写了一个基准测试HashMap 如下:

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class HashMapBenchmark {

  @State(Scope.Benchmark)
  public static class Mystate {
      HashMap<String,String> hashmapVar = new HashMap<String,String>();
      String key0 = "bye";

      @Setup(Level.Iteration)
      public void setup(){
          hashmapVar.put(key0,"bubye");
      }
  }

  @Benchmark
  public void hashmapGet(Mystate state ,Blackhole bh) {
      bh.consume(state.hashmapVar.get(state.key0));
  }

  @Benchmark
  public void hashmapRemove(Mystate state ,Blackhole bh) {
      bh.consume(state.hashmapVar.remove(state.key0));
  }
}

它产生这个结果:

Benchmark                             Mode  Samples  Score  Score error  Units
c.b.HashMapBenchmark.hashmapGet       avgt       60  6.348        0.320  ns/op
c.b.HashMapBenchmark.hashmapRemove    avgt       60  5.180        0.074  ns/op

根据结果,remove()get() 稍快。 即使要删除一个元素,它也必须首先检索该元素,不是吗?

remove() 怎样才能更快?还是我错过了什么?

更新 使用最新的 JMH (1.11.3) 后,结果如下:

Benchmark                                 Mode  Cnt  Score   Error  Units
HashMapBenchmark.hashmapGet               avgt   60  9.713 ± 0.277  ns/op
HashMapBenchmark.hashmapRemove            avgt   60  7.677 ± 0.166  ns/op

【问题讨论】:

  • 您的样本量太小,甚至没有意义。对于此类“轻量级”操作,您通常应该在计算平均值之前进行 tr 和采样 5-10 秒。
  • @Norwæ 他正在使用 jmh 进行 60 次运行,这应该足够好,并且还给出了统计错误。
  • 你用的是什么java版本和jmh版本?
  • @AdamSkywalker jdk1.7.0_01 和 JMH 1.0。我不确定 JHM 版本,因为我使用了 maven 原型:mvn archetype:generate -DinteractiveMode=false -DarchetypeGroupId=org.openjdk.jmh -DarchetypeArtifactId=jmh-java-benchmark-archetype
  • @Ram(我认为这并不重要,但 JMH 版本在您的 POM 中,它是依赖项 jmh-core)如果我没记错的话,它是在 @987654333 中定义的属性 jmh.version @ 部分。

标签: java performance hashmap benchmarking jmh


【解决方案1】:

所以问题是,这些基准测量不同的东西:get() 来自填充的地图,remove() 来自(最终)空地图。比较是没有意义的,你可以把基准扔掉。

您必须保证操作是针对同一个HashMap 完成的。不幸的是,这需要使用 @Setup(Invocation),这本身就很糟糕(阅读 Javadoc!),或者将 HashMap 构建成本吸收到基准测试本身中:

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class HashMapBenchmark {

    @Benchmark
    public String get() {
        HashMap<String, String> hm = createMap();
        return hm.get("bye");
    }

    @Benchmark
    public String remove() {
        HashMap<String, String> hm = createMap();
        return hm.remove("bye");
    }

    // extra protection from optimization
    @CompilerControl(CompilerControl.Mode.DONT_INLINE)
    private HashMap<String, String> createMap() {
        HashMap<String, String> hm = new HashMap<>();
        hm.put("bye", "bye");
        return hm;
    }
}

您可以格外小心,将地图创建剥离为单独的非内联方法:当今的编译器不会跨调用进行优化。在我的 i7-4790K、4.0 GHz、Linux x86_64、JDK 8u66 上:

Benchmark                Mode  Cnt   Score   Error  Units
HashMapBenchmark.get     avgt   15  24.343 ± 0.351  ns/op
HashMapBenchmark.remove  avgt   15  24.611 ± 0.369  ns/op

没有太大的区别。事实上,如果您使用-prof perfasm 查看生成的代码,它会在其中产生一些可量化的差异。或者,您可以使用 -prof perfnorm 快速表征这两种工作负载。

请注意,这种情况没有回答在真实地图上是一种方法还是另一种更好。可以为两者提出论点:get 不会修改映射,因此不会导致内存存储,remove 可能有助于加载因子,以便下一个remove 会变得更快,等等。单个基准测试和一段文本与任何富有成果的讨论相去甚远。

【讨论】:

  • 但是,如果这两种方法都相同,为什么内联地图创建不好呢?只是因为它不会立即内联,只有在一些热身之后?
  • 嗯,这本身还不错,但我可以想象世界末日的场景,其中集合实现足够简单,编译器可以折叠背靠背putget/ 的重要部分remove。这是一种额外的保护——以防您非常懒于研究每个案例的生成代码。
【解决方案2】:

当您在第一次迭代后调用 remove() 时,没有什么要删除的,并且您不必在任何地方复制结果(或者更确切地说是引用结果)(它只是简单地返回 null)。但是当调用get() 时,您必须在某处复制或存储返回的引用(尚未查找Blackhole 的实现),这需要内存分配,因此比仅仅返回null 更昂贵,后者可能会在之后由JIT 优化几次迭代。

【讨论】:

  • 性能分析不是没有证据就可以谈JIT优化的地方
  • 这个误解是关于状态的Level.Iteration设置。使用的正确级别是Invocation,但由于基准太短,这在此处实际上不起作用。
  • @AdamSkywalker 但最重要的是,在第一次调用remove() 之后,地图是空的。您甚至不需要 JIT 来加快速度,因为将完全跳过对存储桶内容的循环。
  • @AdamSkywalker:是的。得到 -> 可能得到。
  • @AdamSkywalker 我不是在争论你的观点,我只是指出,虽然你的具体反对意见是有效的,但答案的主旨(即remove() 测试在一个空的哈希图上运行除了第一次调用之外的所有)都是正确的。
猜你喜欢
  • 2015-01-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-04-23
  • 1970-01-01
  • 1970-01-01
  • 2013-07-01
  • 2016-07-14
相关资源
最近更新 更多