【问题标题】:Performance problems with Clojure HashMap's lookupClojure HashMap 查找的性能问题
【发布时间】:2017-03-25 03:23:06
【问题描述】:

我试图根据这篇论文实现一个纯函数式的 Eratosthenes 算法筛:https://www.cs.hmc.edu/~oneill/papers/Sieve-JFP.pdf

按照所有步骤,我最终得到了一个非常高效的 Haskell 代码,我尝试将它移植到 Clojure。问题是,Clojure 的版本非常慢:就像尝试测试所有数字以检查它们是否可整除一样慢。我最终得到的代码如下:

(defn- sieve2 [[x & xs] table]
  (let [reinsert (fn [table prime]
                  ;  (merge-with concat table {(+ x prime) [prime]})
                   (update table (+ x prime) #(cons prime %)))] ;(vec %) prime)))]
    (if x
      (if-let [facts (get table x)]
        (recur xs (reduce reinsert (dissoc table x) facts))
        (lazy-seq (cons x (sieve2 xs (assoc table (* x x) [x])))))
      '())))

(defn real-sieve [xs] (sieve2 xs {}))

(注释了与 concat 合并,因为那是 Haskell 的方式,但它更慢)。

有了 30000 个素数,Haskell 的版本运行时间为 39 毫秒,而 Clojure 的版本运行时间为 483 毫秒。所以,我将我的 Clojure 版本移植到了 Scala:

val primes2 = {
  def sieve(xs: Stream[Int], table: Map[Int, Vector[Int]]): Stream[Int] =
  xs match {
    case Stream() => xs
    case x #:: xs => table get x match {
      case Some(facts) =>
        sieve(xs, facts.foldLeft(table - x) { (table, prime) =>
          val key = x + prime
          val value = table.getOrElse(key, Vector()) :+ x
          table + (key -> value)
        })
      case None => x #:: sieve(xs, table + (x*x -> Vector(x)))
    }
  }
  sieve(Stream.from(2), Map())
}

它运行时间为 39 毫秒。然后,我下载了 VisualVM 并对我的代码进行了采样,以查看:

请注意,大多数时候,性能杀手是 HashMap 键查找和assoc。我的代码有问题吗?

【问题讨论】:

  • 您是否尝试使用 this map 而不是核心库中的映射?
  • 你试过瞬态吗?
  • 是的,我尝试过使用 int-map,并且得到了相同的结果。不,我没有尝试过瞬态,因为我想要一种纯函数式的方法——Scala 为我提供了不可变的映射,而不会影响性能。
  • 我相信您的 scala 实现中有一个错误,可能会影响其运行时间。在val value = table.getOrElse(key, Vector()) :+ x 行中,您需要添加prime 而不是x。在我的尝试中,这使两个版本的经过时间具有可比性(给定相似的堆大小等)

标签: performance dictionary clojure hashmap


【解决方案1】:

试用 OP 的代码,我确实看到 scala 实现大约需要 30 毫秒,而 clojure 大约需要 500 毫秒。这很奇怪。

所以我比较了结果,发现 scala 实现给了我很多偶数作为素数。经过一番挖掘,我了解到 scala 实现中有两个错误。 第一个:

val value = table.getOrElse(key, Vector()) :+ x     // bug
val value = table.getOrElse(key, Vector()) :+ prime // corrected

这个错误导致计算完成得更快,因为结果中包含了许多非质数。

scala 版本的第二个错误是使用Int。在达到第 30000 个素数之前发生溢出:

scala> 92683*92683
res1: Int = 203897 // an odd square?? 

所以,我也解决了这个问题,由于 scala 没有Stream.from(Long),所以也不得不写(我不会说流利的 scala,所以可能有更好的方法..):

object Test {
  def sieve(xs: Stream[Long], table: Map[Long, Vector[Long]]): Stream[Long] =
        xs match {
            case Stream() => xs
            case x #:: xs = {
              table get x match {
              case Some(facts) =>
                sieve(xs, facts.foldLeft(table - x) { (table, prime) =>
                  val key = x + prime
                  val value = table.getOrElse(key, Vector()) :+ prime
                  table + (key -> value)
                })
                case None =>  {
                     x #:: sieve(xs, table + (x*x -> Vector(x)))
                }}}}   
  def fromLong(start:Long) : Stream[Long] = Stream.cons(start, fromLong(start+1))

  def main(args: Array[String]) {
     sieve(fromLong(2), Map())
  }
}

再次运行它给了我相当的 scala 和 clojure 运行时间:

scala> Test.time {Test.sieve(Test.fromLong(2), Map()).take(30000).last}
Elapsed time: 583 msecs
res14: Long = 350377

还有clojure的版本:

(time (last (take 30000 (real-sieve a))))
"Elapsed time: 536.646696 msecs"
 350377

这实际上是30000th prime!

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-01-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多