Clojure通勤和改变表现

时间:2014-11-11 13:52:15

标签: clojure stm

clojure.org/refs

有一点修改过的例子
(defn mod-nth [v i f] (assoc v i (f (v i))))
(defn run [oper nvecs nitems nthreads niters]
  (let [vec-refs (vec (map (comp ref vec)
                        (partition nitems (repeat (* nvecs nitems) 0))))
        sum  #(reduce + %)
        swap #(let [v1 (rand-int nvecs)
                    v2 (rand-int nvecs)
                    i1 (rand-int nitems)
                    i2 (rand-int nitems)]
                (dosync
                  (let [temp (nth @(vec-refs v1) i1)]
                    (oper (vec-refs v1) mod-nth i1 inc)
                    (oper (vec-refs v2) mod-nth i2 dec))))
        report #(do
                  (prn (map deref vec-refs))
                  (println "Sum:"
                    (reduce + (map (comp sum deref) vec-refs))))]
    (report)
    (dorun (apply pcalls (repeat nthreads #(dotimes [_ niters] (swap)))))
    (report)))

(time (run alter 100 10 10 100000))

样本输出

([0 0 0 0 0 0 0 0 0 0] [...])
Sum: 0
([15 -14 -8 57 -26 -12 -49 -29 33 -3] [...])
Sum: 0
"Elapsed time: 1995.938147 msecs"

我没有交换唯一的数字,而是将其从一个向量元素转移到另一个向量元素。

这个操作可以假设是可交换的,所以还有另一个测试 - 除了使用commute而不是alter

之外,它是相同的
(time (run commute 100 10 10 100000))

样本输出如

([0 0 0 0 0 0 0 0 0 0] [...])
Sum: 0
([8 48 -10 -41 -17 -32 -4 50 -31 88] [...])
Sum: 0
"Elapsed time: 3141.591517 msecs"

令人惊讶的是,第一个示例在大约2 seconds中运行而第二个需要3 seconds

但如上所述in this SO answer

  

commute是alter的优化版本,适用于事情顺序无关紧要的时候

如何在这个简单的情况下需要更多时间进行同样的工作来优化它? commute的目的是什么?

2 个答案:

答案 0 :(得分:4)

我使用VisualVM来监控使用clojure.corealter运行示例时涉及的commute函数。

alter

alter

commute

commute

如果我对结果的解释是正确的,则在每个函数上花费的累计时间表明commute实际上比alter更快。似乎并行运行代码所需的所有其他操作的开销是使性能陷入混乱的开销。

基准测试代码非常棘手,使用time有时会产生误导。 VisualVm提供的信息可能甚至不是最终的词,分析和使用criterium等工具可能是确保结果值得信赖的最佳方法。

另一个重要的事实是,在dosync区块内完成的操作不会花费那么长时间,所以即使其中一个重试,这涉及的额外时间并不那么重要。在dosync内添加一点延迟会使重试(alter)与不重试(commute)之间的区别更加显着。

(defn mod-nth [v i f] (assoc v i (f (v i))))
(defn run [oper nvecs nitems nthreads niters]
  (let [vec-refs (vec (map (comp ref vec)
                        (partition nitems (repeat (* nvecs nitems) 0))))
        sum  #(reduce + %)
        swap #(let [v1 (rand-int nvecs)
                    v2 (rand-int nvecs)
                    i1 (rand-int nitems)
                    i2 (rand-int nitems)]
               (dosync
                 (let [temp (nth @(vec-refs v1) i1)]
                   (Thread/sleep 1)                     ;; This was added
                   (oper (vec-refs v1) mod-nth i1 inc)
                   (oper (vec-refs v2) mod-nth i2 dec))))
        report #(do
                  (prn (map deref vec-refs))
                  (println "Sum:"
                    (reduce + (map (comp sum deref) vec-refs))))]
    (doall (apply pcalls (repeat nthreads #(dotimes [_ niters] 
                                            (swap)))))))

(time (run alter 100 10 10 5000))
;= "Elapsed time: 15252.427 msecs"
(time (run commute 100 10 10 5000))
;= "Elapsed time: 13595.399 msecs"

答案 1 :(得分:2)

了解commute所做的优化具体是非常重要的: commute避免在alter需要的情况下不必要地重新运行块内的代码抛弃结果。

未指定commutealter的实现之间的常量因素开销,因此您在此处看到的内容违反了Clojure规范的任何部分。也就是说 - 随着dosync块内个别交易所花费的时间增加,使用alter时使用commute的代价会增加,同样会增加{/ 1}}。

一般来说:

  • 微观标记是邪恶的(从某种意义上说,它们会鼓励那些不适合现实世界使用的不良行为)。注意现实场景中的性能行为,而不是人为的测试用例。
  • 尽可能在Clojure的STM中使用commute