【问题标题】:Clojure performance on set updates or large number computationsClojure 在集合更新或大量计算上的性能
【发布时间】:2014-08-16 16:32:24
【问题描述】:

我正在尝试使用筛算法总结欧拉项目问题的素数。我正在使用一个可变集合来存储不是素数的数字,并使用“dosync”和“commute”来更新该集合(否则,如果它是不可变的,我会用完内存)。在 120 万个素数之前性能大致呈线性关系,但在 150 万个时性能很糟糕(7 秒对 64 秒)。任何想法我做错了什么?我的猜测是数字可能变得太大,或者更新可变集效率低下。

(defn mark-multiples [not-prime-set multiple prime-max]
  (loop [ counter (long 2) 
      product (* counter multiple)]
    (if (> product prime-max) not-prime-set
      (do
        (dosync (commute not-prime-set conj product))
        (recur  (inc counter) (* (inc counter) multiple))))))


(defn sieve-summation [prime-max]
  (def not-prime-set (ref #{ (long 1) }) )
  (loop [counter (long 2)
     summation (long 0)]
    (if (> counter prime-max) summation
      (if (not (contains? @not-prime-set counter)) 
        (do 
          (mark-multiples not-prime-set counter prime-max)
          (recur  (inc counter) (+ summation counter)))
        (recur (inc counter) summation)))))

=>(时间(筛子总和 100000)) “经过时间:496.673 毫秒” 454396537

=>(时间(筛子总和 150000)) “经过时间:763.333 毫秒” 986017447

=>(时间(筛子总和 1000000)) “经过时间:6037.926 毫秒” 37550402023

=> (时间 (筛子总和 1100000)) “经过时间:6904.385 毫秒” 45125753695

=> (时间 (筛子总和 1200000)) “经过时间:7321.299 毫秒” 53433406131

=> (时间 (筛子总和 1500000)) “经过时间:64995.216 毫秒” 82074443256

----编辑----

感谢 A. Webb,很好的建议!你的代码有点慢,所以为了让它加速,我必须在一开始就制作非素数集瞬态,现在它运行得更快(大约 8 倍)。我仍然遇到内存不足的错误,所以我将尝试弄清楚如何增加 jvm 上的堆大小以查看是否可以解决它。我在 Mac 上的 Eclipse 上运行 Clojure,而且我是 Clojure 和 Mac 的新手。

我很想看看您如何进一步重构程序(保持基本相同的逻辑)以在 Clojure 中更加优雅。再次感谢。

(defn mark-multiples2 [not-prime-set prime prime-max]
  (loop [multiple (* 2 prime) nps not-prime-set ]
    (if (> multiple prime-max) 
      nps
      (recur (+ multiple prime) (conj! nps multiple)))))


(defn sieve-summation2 [prime-max]
  (loop [counter 2, summation 0, not-prime-set (transient #{1})]
    (if (> counter prime-max) 
      summation
      (if (not-prime-set counter) 
        (recur (inc counter) summation not-prime-set)
        (recur (inc counter) 
           (+ summation counter) 
           (mark-multiples2 not-prime-set counter prime-max))))))

=> (时间 (sieve-summation2 100000)) “经过时间:124.781 毫秒” 454396537

=>(时间(筛子总和 100000)) “经过时间:876.744 毫秒” 454396537

【问题讨论】:

  • 你的 jvm 的堆大小是多少?我没有看到 -Xmx1G 的退化
  • 您应该使用 Clojure 1.6 - 哈希已经过大修,对于这个特定的用例可能会更好。
  • 谢谢,我试试看。对于在 Mac 上运行 Clojure,你们都推荐什么?我感谢任何支持我的声誉的投票:)
  • Leiningen (lein) 是除了您选择的编辑器之外您所需要的一切。直接从站点安装,因为包管理器提供过时的版本。

标签: performance clojure


【解决方案1】:

在 Clojure 中有很多更好、更优雅的方法可以解决这个问题,但这不是你问题的重点。

使用引用类型——无论它是一个引用,或者更恰当地说是一个原子——在这里对你没有任何作用。你仍在制造同样多的垃圾。您只是将可变存储位置的内容从一个不可变数据结构交换到另一个。我不知道是什么导致了你的时间峰值,但一种可能是你触发了一个很长的垃圾回收周期。

你想在这里使用的是transients。在不过多更改代码的情况下,以下应该是显着的加速。

(defn mark-multiples [not-prime-set multiple prime-max]
  (loop [m (* 2 multiple), nps (transient not-prime-set)]
    (if (> m prime-max) 
      (persistent! nps)
      (recur (+ m multiple) (conj! nps m)))))


(defn sieve-summation [prime-max]
  (loop [counter 2, summation 0, not-prime-set #{1}]
    (if (> counter prime-max) 
      summation
      (if (contains? not-prime-set counter) 
        (recur (inc counter) summation not-prime-set)
        (recur (inc counter) 
               (+ summation counter) 
               (mark-multiples not-prime-set counter prime-max))))))

这是相同的算法,风格更惯用:

(defn mark [s n m]
  (into s (range (* 2 n) m n)))

(defn prime-sum [m]
  (let [step (fn [[a s] n] 
               (if (s n)
                 [a s] 
                 [(+ a n) (mark s n m)]))]
  (first (reduce step [0 #{}] (range 2 m)))))

从这里开始,您可能会开始解决算法固有的内存问题——您正在存储所有非素数,而您只需要在任何给定点存储下一个非素数。有关该想法的完美实现,请参阅 Christophe Grand 的 Everybody loves the Sieve of Eratosthenes 条目。

【讨论】:

  • 我用代码编辑了我上面的问题,以使其更快。感谢您提出任何其他建议,谢谢。
  • 感谢分享惯用的风格。我花了一些时间才弄清楚 reduce 是如何工作的,非常巧妙。
【解决方案2】:

我使用了 Eclipse 的逆时针插件,它使用的是 Clojure 1.5,我相信因为我有 jdk 1.6。我升级到 jdk 1.7,并更新了 project.clj 以使用 Clojure 1.6.0,内存/速度问题都不是问题。 感谢您的建议。

【讨论】:

    猜你喜欢
    • 2018-02-16
    • 2020-07-20
    • 2013-05-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-06-14
    • 2012-05-19
    相关资源
    最近更新 更多