【问题标题】:Java : Iteration through a HashMap, which is more efficient?Java:通过 HashMap 进行迭代,哪个更高效?
【发布时间】:2011-08-15 03:29:26
【问题描述】:

鉴于以下代码,有两种替代方法可以迭代它,
这两种方法之间有什么性能差异吗?

        Map<String, Integer> map = new HashMap<String, Integer>();
        //populate map

        //alt. #1
        for (String key : map.keySet())
        {
            Integer value = map.get(key);
            //use key and value
        }

        //alt. #2
        for (Map.Entry<String, Integer> entry : map.entrySet())
        {
            String key = entry.getKey();
            Integer value = entry.getValue();
            //use key and value
        }

我倾向于认为alt. #2 是遍历整个map 的更有效方法(但我可能错了)

【问题讨论】:

  • 地图到底有多大?这听起来像是过早的优化。
  • @Matt 我问是因为我有几个,而且它们很大——通常是 10K-100K 元素;肯定有一个很好的优化案例!
  • 更新:许多答案似乎认为这是过早的优化。请注意,上面确实是 SSCCE (sscce.org),而不是我要优化的实际代码!
  • 为什么会出现这种过早优化?第二种选择显然更快,但也显然更自我记录。如果你想要的只是值,你还会使用键集迭代器吗?我希望不会。
  • 我认为如果密钥很大和/或涉及非常多的 .equals ,这将成为一个更严重的问题 基准测试会很有趣:如何涉及 .equals 方法以便第二个选项至少慢两倍...

标签: java map performance hashmap iteration


【解决方案1】:

您的第二个选项肯定更有效,因为与第一个选项中的 n 次相比,您只进行一次查找。

但是,没有什么比尽可能尝试更好的了。所以这里 -

(不完美,但足以验证假设并在我的机器上)

public static void main(String args[]) {

    Map<String, Integer> map = new HashMap<String, Integer>();
    // populate map

    int mapSize = 500000;
    int strLength = 5;
    for(int i=0;i<mapSize;i++)
        map.put(RandomStringUtils.random(strLength), RandomUtils.nextInt());

    long start = System.currentTimeMillis();
    // alt. #1
    for (String key : map.keySet()) {
        Integer value = map.get(key);
        // use key and value
    }
    System.out.println("Alt #1 took "+(System.currentTimeMillis()-start)+" ms");

    start = System.currentTimeMillis();
    // alt. #2
    for (Map.Entry<String, Integer> entry : map.entrySet()) {
        String key = entry.getKey();
        Integer value = entry.getValue();
        // use key and value
    }
    System.out.println("Alt #2 took "+(System.currentTimeMillis()-start)+" ms");
}

结果(一些有趣的)

int mapSize = 5000; int strLength = 5;
Alt #1 耗时 26 毫秒
Alt #2 耗时 20 毫秒

int mapSize = 50000; int strLength = 5;
Alt #1 耗时 32 毫秒
Alt #2 耗时 20 毫秒

int mapSize = 50000; int strLength = 50;
Alt #1 耗时 22 毫秒
Alt #2 耗时 21 毫秒

int mapSize = 50000; int strLength = 500;
Alt #1 耗时 28 毫秒
Alt #2 耗时 23 毫秒

int mapSize = 500000; int strLength = 5;
Alt #1 耗时 92 毫秒
Alt #2 耗时 57 毫秒

...等等

【讨论】:

  • 请谷歌了解如何进行有效的微基准测试。 (关键点:让热点在基准测试之前做一些预热。)
  • @Paulo - 足够公平并指出。我重试了使用预热阶段(基本上运行整个序列一次,然后再次运行它进行测量),但结果非常一致。我想这是因为即使没有预热阶段,看跌期权也在预热?
  • +1 @amol:感谢基准测试/确凿的证据@Paulo:您会推荐什么特定标准进行基准测试?
  • 这已经很老了,但是当有人问到微基准测试时,有一个讨论它的线程:stackoverflow.com/questions/504103/…
【解决方案2】:

第二个 sn-p 会稍微快一些,因为它不需要重新查找密钥。

所有HashMap 迭代器调用nextEntry method,它返回一个Entry&lt;K,V&gt;

您的第一个 sn-p 丢弃条目中的值(在 KeyIterator 中),然后在字典中再次查找它。

您的第二个 sn-p 直接使用键和值(来自 EntryIterator

keySet()entrySet() 都是廉价电话)

【讨论】:

    【解决方案3】:

    地图:

    Map&lt;String, Integer&gt; map = new HashMap&lt;String, Integer&gt;();

    除了这两个选项,还有一个。

    1) keySet() - 如果您需要使用 keys

    ,请使用它
    for ( String k : map.keySet() ) {
        ...
    }
    

    2) entrySet() - 如果您需要两者,请使用它:keys & values

    for ( Map.Entry<String, Integer> entry : map.entrySet() ) {
        String k = entry.getKey();
        Integer v = entry.getValue();
        ...
    }
    

    3) values() - 如果您需要 values

    ,请使用它
    for ( Integer v : map.values() ) {
        ...
    }
    

    【讨论】:

      【解决方案4】:

      后者比前者更有效率。 FindBugs 之类的工具实际上会标记前者并建议您使用后者。

      【讨论】:

      • +1 @Jonas:感谢您提到 FindBugs - 每天学习新东西!
      【解决方案5】:

      最有效的方法(根据我的基准测试)是使用 Java 8 中添加的新 HashMap.forEach() 方法或 HashMap.entrySet().forEach()

      JMH 基准测试:

      @Param({"50", "500", "5000", "50000", "500000"})
      int limit;
      HashMap<String, Integer> m = new HashMap<>();
      public Test() {
      }
      @Setup(Level.Trial)
      public void setup(){
          m = new HashMap<>(m);
          for(int i = 0; i < limit; i++){
              m.put(i + "", i);
          }
      }
      int i;
      @Benchmark
      public int forEach(Blackhole b){
          i = 0;
          m.forEach((k, v) -> { i += k.length() + v; });
          return i;
      }
      @Benchmark
      public int keys(Blackhole b){
          i = 0;
          for(String key : m.keySet()){ i += key.length() + m.get(key); }
          return i;
      }
      @Benchmark
      public int entries(Blackhole b){
          i = 0;
          for (Map.Entry<String, Integer> entry : m.entrySet()){ i += entry.getKey().length() + entry.getValue(); }
          return i;
      }
      @Benchmark
      public int keysForEach(Blackhole b){
          i = 0;
          m.keySet().forEach(key -> { i += key.length() + m.get(key); });
          return i;
      }
      @Benchmark
      public int entriesForEach(Blackhole b){
          i = 0;
          m.entrySet().forEach(entry -> { i += entry.getKey().length() + entry.getValue(); });
          return i;
      }
      public static void main(String[] args) throws RunnerException {
          Options opt = new OptionsBuilder()
                  .include(Test.class.getSimpleName())
                  .forks(1)
                  .warmupIterations(25)
                  .measurementIterations(25)
                  .measurementTime(TimeValue.milliseconds(1000))
                  .warmupTime(TimeValue.milliseconds(1000))
                  .timeUnit(TimeUnit.MICROSECONDS)
                  .mode(Mode.AverageTime)
                  .build();
          new Runner(opt).run();
      }
      

      结果:

      Benchmark            (limit)  Mode  Cnt      Score    Error  Units
      Test.entries              50  avgt   25      0.282 ±  0.037  us/op
      Test.entries             500  avgt   25      2.792 ±  0.080  us/op
      Test.entries            5000  avgt   25     29.986 ±  0.256  us/op
      Test.entries           50000  avgt   25   1070.218 ±  5.230  us/op
      Test.entries          500000  avgt   25   8625.096 ± 24.621  us/op
      Test.entriesForEach       50  avgt   25      0.261 ±  0.008  us/op
      Test.entriesForEach      500  avgt   25      2.891 ±  0.007  us/op
      Test.entriesForEach     5000  avgt   25     31.667 ±  1.404  us/op
      Test.entriesForEach    50000  avgt   25    664.416 ±  6.149  us/op
      Test.entriesForEach   500000  avgt   25   5337.642 ± 91.186  us/op
      Test.forEach              50  avgt   25      0.286 ±  0.001  us/op
      Test.forEach             500  avgt   25      2.847 ±  0.009  us/op
      Test.forEach            5000  avgt   25     30.923 ±  0.140  us/op
      Test.forEach           50000  avgt   25    670.322 ±  7.532  us/op
      Test.forEach          500000  avgt   25   5450.093 ± 62.384  us/op
      Test.keys                 50  avgt   25      0.453 ±  0.003  us/op
      Test.keys                500  avgt   25      5.045 ±  0.060  us/op
      Test.keys               5000  avgt   25     58.485 ±  3.687  us/op
      Test.keys              50000  avgt   25   1504.207 ± 87.955  us/op
      Test.keys             500000  avgt   25  10452.425 ± 28.641  us/op
      Test.keysForEach          50  avgt   25      0.567 ±  0.025  us/op
      Test.keysForEach         500  avgt   25      5.743 ±  0.054  us/op
      Test.keysForEach        5000  avgt   25     61.234 ±  0.171  us/op
      Test.keysForEach       50000  avgt   25   1142.416 ±  3.494  us/op
      Test.keysForEach      500000  avgt   25   8622.734 ± 40.842  us/op
      

      如您所见,HashMap.forEachHashMap.entrySet().forEach() 在大地图上表现最佳,并通过 entrySet() 上的 for 循环加入,以获得在小地图上的最佳性能。

      keys 方法较慢的原因可能是因为它们必须为每个条目再次查找值,而其他方法只需要读取他们已经必须获取值的对象中的字段。我希望迭代器方法变慢的原因是它们正在进行外部迭代,这需要对每个元素进行两次方法调用(hasNextnext),并将迭代状态存储在迭代器对象中,而forEach 完成的内部迭代只需要对accept 进行一次方法调用。

      您应该使用目标数据在目标硬件上进行概要分析,并在循环中执行目标操作以获得更准确的结果。

      【讨论】:

        【解决方案6】:

        一般来说,对于 HashMap,第二个会快一点。只有当你有很多哈希冲突时才真正重要,因为那时get(key)调用比O(1)慢 - 它得到O(k)k是同一个桶中的条目数(即具有相同哈希码或不同哈希码的键仍然映射到同一个存储桶 - 这也取决于映射的容量、大小和负载因子。

        Entry-iterating 变体不必进行查找,因此它在这里变得更快。

        另一个注意事项:如果您的地图的容量比实际大小大很多并且您经常使用迭代,您可以考虑使用 LinkedHashMap 代替。它为完整的迭代(以及可预测的迭代顺序)提供O(size) 而不是O(size+capacity) 复杂性。 (您仍然应该衡量这是否真的带来了改进,因为这些因素可能会有所不同。LinkedHashMap 创建地图的开销更大。)

        【讨论】:

          【解决方案7】:

          bguiz,

          我认为(我不知道)迭代 EntrySet(备选方案 2)的效率略高,仅仅是因为它不会散列每个键以获得它的值...话虽如此,计算散列是每个条目的 O(1) 操作,因此我们只在整个 HashMap...可能具有非常不同的性能特征。

          我确实认为您会“推动”它以真正注意到性能差异。如果您担心,那么为什么不设置一个测试用例来为这两种迭代技术计时?

          如果您没有报告过真实的性能问题,那么您真的担心的不是很多......这里和那里的几个时钟滴答声不会影响程序的整体可用性。

          我相信代码的许多其他方面通常比直接性能更重要。当然,有些块是“性能关键的”,这在编写之前就已经知道了,更不用说性能测试了……但这种情况相当罕见。作为一种通用方法,最好专注于编写完整、正确、灵活、可测试、可重用、可读、可维护的代码……性能可以在以后根据需要构建。

          版本 0 应该尽可能简单,没有任何“优化”。

          【讨论】:

          • 请注意,这绝对不是过早优化的情况,软件绝对不是零版本。它是现有的成熟软件,需要性能改进。在我的问题中,我发布的是 SSCCE (sscce.org)
          猜你喜欢
          • 2016-01-08
          • 1970-01-01
          • 2015-06-16
          • 2019-04-30
          • 1970-01-01
          • 2015-09-19
          • 2011-03-21
          • 1970-01-01
          • 2021-04-18
          相关资源
          最近更新 更多