在HashMap中,remove()是否比get()快?

2022-09-04 06:50:59

我已经写了一个基准和如下:getremoveHashMap

@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

答案 1

所以麻烦的是,这些基准测试测量不同的东西:从填充的地图,到(最终)空的地图。比较毫无意义,您可能会抛弃基准。get()remove()

您必须保证操作是针对相同的.不幸的是,这需要使用 本身就很糟糕(阅读Javadoc!),或者将构建成本吸收到基准测试本身中:HashMap@Setup(Invocation)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

请注意,这种情况并不能回答一种方法或另一种方法在真实地图上是否更好。可以对两者进行论证:不修改映射,因此不会导致内存存储,可能有助于负载因子,以便下一步会变得更快,等等。一个单一的基准和一段文字远非任何富有成果的讨论。getremoveremove


答案 2

当您在第一次迭代后调用时,无需删除任何内容,也不必将结果(或更确切地说是对结果的引用)复制到任何地方(它只是简单地返回)。但是,在调用时,您必须将返回的引用复制或存储在某个地方(尚未查找)的实现,这需要内存分配,因此比简单地返回更昂贵,这可能在几次迭代后通过JIT进行优化。remove()nullget()Blackholenull


推荐