Clojure:在多个Future中运行计算不会加速我的程序

3

我刚开始学习Clojure,写了以下代码来通过蒙特卡洛模拟估算pi。我想创建X个线程,每个线程计算落在单位圆内的随机点数,并返回它。然后我的主线程对它们加总并计算π。

然而,使用同一个线程运行所有样本比通过futures将计算分配给几个线程更快。为什么呢?

(defn sumFutures [workerFutures acc i]
  (if (= -1 i)
    acc
    (recur workerFutures (+ acc @(nth workerFutures i)) (dec i))))

(defn getResults [workerFutures numSamples]
  (* 4.0 (/ (sumFutures workerFutures 0 (dec (count workerFutures))) numSamples)))

(defn isInCircle []
  (let [x (rand) y (rand)]
    (if (<= (+ (* x x) (* y y)) 1)
      1
      0)))

(defn countInCircle [remaining acc]
  (if (zero? remaining)
    acc
    (recur (dec remaining) (+ acc (isInCircle)))))

(defn getWorker [samplesPerWorker]
  (future
    (countInCircle samplesPerWorker 0)))

(defn addWorker [workers samplesPerWorker]
  (conj workers (getWorker samplesPerWorker)))

(defn getWorkers [workers samplesPerWorker remWorkers]
  (if (not (zero? remWorkers))
    (recur (addWorker workers samplesPerWorker) samplesPerWorker (dec remWorkers))
    (doall workers)))

(defn main [numSamples numWorkers]
  (getResults (getWorkers [] (quot numSamples numWorkers) numWorkers) numSamples))

;; Run all in 1 thread
(main 1000000 1)

;; Split among 100 futures (at least 8 threads)
;; SLOWER 
(main 1000000 100)

根据调试结果,以下是一些注意事项:

  • 创建了正确数量的 futures

  • 每个 future 正确计算了模拟次数

  • 该过程在多个线程和处理器核心上运行


在第二个主调用中,似乎你要求每个工作进程处理1M个numSamples - 你不应该按照numWorkers的数量进行分割吗? - Piotrek Bzdyl
样本数每个工人计算在主函数中,使用(quot numSamples numWorkers)进行计算。numSamples始终是样本的总数。 - DinoEntrails
你运行了多少个核心/处理器? - Bob Jarvis - Слава Україні
2个回答

4

如果您使用惯用代码,那么这将更容易处理。

sumFutures 实际上重新实现了 Clojure 的 + 定义,直接使用递归而不是 Clojure 优化的 reduce 实现。以下是另一种更简单(可能更快)的定义:

(defn sum-futures
  [workers]
  (apply + (map deref workers)))

getResults现在更易于阅读 - 我发现了一个进行有理数除法的地方 - 如果我们不需要有理数,将一个操作数设置为double类型可以节省很多工作。

(defn get-results
  [workers num-samples]
  (* 4.0 (/ (sum-futures workers)
            (double num-samples))))

countInCircle可以使用Clojure的+更清晰地表示。

(defn count-in-circle
  [n]
  (apply + (repeatedly n isInCircle)))
getWorkers再次执行了原始递归操作而不使用Clojure的抽象。如果我们使用repeatedly,我们可以消除addWorkergetWorker的定义,同时不降低清晰度、模块化或效率(事实上,在这种情况下,如果您不需要索引查找,并且结果将按顺序进行消耗,lazy-seq版本应该比向量执行得更好。这也是现在需要进行重构的候选项,以及与sum-futures一起转换为更有效的转换器版本)。

(defn get-workers
  [num-workers samples-per-worker]
  (repeatedly num-workers
              #(future (count-in-circle samples-per-worker))

最后,main 变成了:
(defn main
  [num-samples num-workers]
  (get-results (get-workers (quot num-samples num-workers)
                            num-workers)
               num-workers))

0
如果你在第二次运行中将未来的数量减少到2(和8),会发生什么?可能线程和未来管理所花费的时间大于通过分割工作节省的时间。
生成比硬件支持的线程更多通常是适得其反的(因为线程必须共享核心,这是有成本的)。如果只有两个线程(与1相比)没有速度增益,那么可能其他地方出了问题。

看起来使用2个线程可以加速,但是3个或更多会减慢速度。这让我感到困惑,因为等效的Java实现根本不显示这种行为。它会加速直到我达到大约10个线程。 - DinoEntrails
由于某些原因,创建(和/或切换)未来线程的速度会变慢。如果您添加更多的点以降低工作与线程管理的相对重要性,那么您应该再次达到大约10个的阈值。 - Joanis

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接