【问题标题】:Why is Scala hashmap slow?为什么 Scala 哈希图很慢?
【发布时间】:2015-04-28 23:30:57
【问题描述】:

对此我们能做些什么?

我已经运行了一些测试,似乎 Scala Hashmap 比 Java HashMap 慢得多。请证明我错了!

对我来说,Hashmap 的全部意义在于快速访问给定键的值。因此,当速度很重要时,我发现自己求助于使用 Java HashMap,这有点令人难过。我没有足够的经验可以肯定地说,但似乎你将 Java 和 Scala 混合得越多,你可能面临的问题就越多。

test("that scala hashmap is slower than java") {
    val javaMap = new util.HashMap[Int,Int](){
      for (i <- 1 to 20)
      put(i,i+1)
    }

    import collection.JavaConverters._
    val scalaMap = javaMap.asScala.toMap

    // check is a scala hashmap
    assert(scalaMap.getClass.getSuperclass === classOf[scala.collection.immutable.HashMap[Int,Int]])

    def slow = {
      val start = System.nanoTime()
      for (i <- 1 to 1000) {
        for (i <- 1 to 20) {
          scalaMap(i)
        }
      }
      System.nanoTime() - start
    }

    def fast = {
      val start = System.nanoTime()
      for (i <- 1 to 1000) {
        for (i <- 1 to 20) {
          javaMap.get(i)
        }
      }
      System.nanoTime() - start
    }

    val elapses: IndexedSeq[(Long, Long)] = {
      (1 to 1000).map({_ => (slow,fast)})
    }

    var elapsedSlow = 0L
    var elapsedFast = 0L
    for ((eSlow,eFast) <- elapses) {
      elapsedSlow += eSlow
      elapsedFast += eFast
    }

    assert(elapsedSlow > elapsedFast)

    val fraction : Double = elapsedFast.toDouble/elapsedSlow
    println(s"slower by factor of: $fraction")
}

我错过了什么吗?

答案总结

到目前为止,在比较 Java 8 和 Scala 2.11 时,Java HashMap 似乎在查找(对于少量键的情况下)比 Scala 产品更快——除了 LongMap(如果您的键是 Ints/多头)。

性能差异并没有那么大,以至于在大多数用例中都很重要。希望 Scala 将提高他们的地图的速度。同时,如果您需要性能(使用非整数键),请使用 Java。

Int 键,n=20
Long(60)、Java(93)、Open(170)、MutableSc(243)、ImmutableSc(317)

案例对象键,n=20
Java(195),AnyRef(230)

【问题讨论】:

  • 如果你要使用 Java 地图,我推荐使用 import scala.collection.JavaConversions._

标签: scala hashmap java-8 scala-2.11


【解决方案1】:

Scala 2.13(2019 年 6 月)确实引入了新的、更快的 HashMap/Set 实现

不可变 (d5ae93e) 和可变 (#7348) 版本均已完全替换。 - 在大多数情况下,它们的性能大大优于旧的实现。 - 可变版本现在的性能与 Java 标准库的实现相当。

对于不可变的HashSetHashMap

重新实现基于压缩哈希数组映射前缀树CHAMP)。

请参阅 Steindorfer 和 Vinju 撰写的论文“Optimizing Hash-Array Mapped Tries for Fast and Lean Immutable JVM Collections” (OOPSLA'15),了解有关低级性能优化的更多详细信息和描述 (a pre-print of the paper is available)。

【讨论】:

    【解决方案2】:

    首先:使用 nanoTime 进行 JVM 基准测试非常容易出错。使用微基准测试框架,例如 ThymeCaliperJMH

    第二:你正在比较一个 mutable java hash map 和一个 immutable scala hash map。不可变集合可以非常快,但在某些情况下它们永远不会像可变数据结构那样快。

    这里是可变 java 哈希映射与不可变 scala 哈希映射的适当微基准:https://gist.github.com/rklaehn/26c277b2b5666ec4b372

    如您所见,scala 不可变映射比 java 可变映射快一点。请注意,一旦您转到更大的地图,情况就不会如此,因为不可变的数据结构必须做出一些妥协才能启用structural sharing。我猜想在这两种情况下,主要的性能问题是将整数装箱为整数。

    更新:如果您真的想要一个以整数作为键的可变哈希 hap,那么 scala 集合库中的正确选择是 scala.collection.mutable.LongMap。这使用 long as 键,并且比通用 Map 具有更好的性能,因为它不必对值进行装箱。查看 gist 的结果。

    更新 2:如果您的密钥从 AnyRef 扩展(例如字符串),则高性能 可变 映射的最佳选择是 scala.collection.mutable.AnyRefMap

    【讨论】:

    • 你能解释和/或引用为什么使用System.nanoTime()extremely error-prone 的来源吗?
    • 这是一个关于依赖 System.nanoTime() 进行基准测试的陷阱(以及一般 JVM 基准测试的困难)的非常好的演示:shipilev.net/blog/2014/nanotrusting-nanotime
    • 基本上问题是:你需要给 JVM 足够的时间来优化代码(所谓的预热),你需要确保基准测试没有被完全优化掉。这就是基准方法必须始终返回结果的原因。
    • @Rüdiger 感谢您的出色回应,但是,我已经尝试了您的测试,并进行了一项修改:我删除了累积计算:r += javaMap.get(i),因此在每种情况下都改为r = javaMap.get(i)。我还出于兴趣添加了 OpenHashMap。结果与你自己的不符!我得到:Java 127.9 vs Scala Immutable 534.5,Java 126.8 vs Scala Long 88.59,Java 126.2 vs Scala Open 245.2。因此,Scala 仅在使用 LongMap 时获胜,而在其他情况下则明显更差。但我认为原因归结为@mohit 给出的原因
    • @RüdigerKlaehn - OP 运行每个函数 1000 次。他的每个函数运行get 又调用了1000 次。 OP 然后聚合两个函数的结果,然后将其除以得到与取平均值相同的因子。 JVM通过多次运行函数而被预热,并且函数返回时间,那么为什么他的方法是错误的?
    【解决方案3】:

    而不是调用apply,即scalaMap(i),如果你调用scalaMap.get(i),那么它和javaMap.get(i)一样快

    来自source,申请代码是

    
    def apply(key: A): B = get(key) match {
        case None => default(key)
        case Some(value) => value
      }
    

    这表明 apply 方法首先调用get 方法,然后对其进行模式匹配。在option 的情况下,每次呼叫都有一个额外的跳跃确实会降低性能,并且已经在 SO 上进行了讨论(虽然找不到链接)

    【讨论】:

    • 你试过了吗?我得到了与apply 相似的基准测试结果。
    • @ArieShaw 是的,我试过了。我正在使用 scala-2.11,几乎没有任何性能损失。有时,scalaMap 更快(由 JVM 提供)
    • @mohit 谢谢,我早该想到的!虽然我认为这让你慢下来的主要原因是创建一个对象:Some - 而不是模式匹配。
    • @MS-H - 每当我测试时,调用 scalaMap.get(i) 和 javaMap.get(i) 都没有显示出差异。调用 apply 有很好的性能损失。如果 apply 后没有模式匹配,则与直接调用 get 相同。额外的步骤是模式匹配,这让我认为这是导致缓慢的原因。
    • 有些是在 get 方法本身中创建的。 case 语句只是调用 unapply..
    猜你喜欢
    • 1970-01-01
    • 2015-04-16
    • 1970-01-01
    • 1970-01-01
    • 2010-11-25
    • 1970-01-01
    • 2016-03-01
    • 1970-01-01
    • 2011-04-25
    相关资源
    最近更新 更多