在Clojure中,我如何用转换器实现"频率"的性能版本



(问题来源:Fernando Abrao。)

我听说Clojure中换能器的性能优势,但我不知道如何使用它们。

假设我有一个qos/device-qos-range函数,它返回映射序列,其中一些映射包含十进制:samplevalue,如下所示:

[
{ :samplevalue 1.3, ... },
{ :othervalue -27.7, ... },
{ :samplevalue 7.5, ... },
{ :samplevalue 1.9, ... },
]

我想看看每个整数仓中有多少:samplevalue,如下所示:

(frequencies
(reduce #(if (not (nil? (:samplevalue %2)))
(conj %1 (.intValue (:samplevalue %2))))
[]
(qos/device-qos-range origem device qos alvo inicio fim)))
;; => {1 2, 7 1}

如何将其转换为具有消除中间数据结构(如reduce返回的数据结构)的转换器的快速版本?可以利用多个内核进行并行处理的代码的奖励积分。

(答案来源:Renzo Borgatti(@reberg)。)

首先,让我们设置一些示例数据,稍后将使用这些数据进行性能测试。此矢量包含具有相同关键点的500k个贴图。值重叠了五分之一的时间。

(def data 
(mapv hash-map 
(repeat :samplevalue) 
(concat (range 1e5)
(range 1e5)
(range 1e5)
(range 1e5)
(range 1e5))))

现在,让我们用换能器来完成变换。请注意,此解决方案是而不是并行的。我把你的.intValue缩短为int,这也起到了同样的作用。此外,从每个映射中有条件地获取:samplevalue可以缩短为仅(keep :samplevalue sequence),这相当于(remove nil? (map :samplevalue sequence))。我们将使用Criterium进行基准测试。

(require '[criterium.core :refer [quick-bench]])
(quick-bench
(transduce
(comp
(keep :samplevalue)
(map int))
(completing #(assoc! %1 %2 (inc (get %1 %2 0))) persistent!)
(transient {})
data))
;; My execution time mean: 405 ms

请注意,这次我们没有将frequencies作为外部步骤调用。相反,我们把它融入了行动中。就像frequencies所做的一样,为了获得额外的性能,我们对瞬态哈希图进行了操作。我们通过使用一个瞬态哈希图作为种子,并通过调用persistent!来使用completing作为最终值

我们可以将其进行比较。为了获得最大性能,我们使用了可变的JavaConcurrentHashMap,而不是不可变的Clojure数据结构。

(require '[clojure.core.reducers :as r])
(import '[java.util HashMap Collections Map]
'java.util.concurrent.atomic.AtomicInteger
'java.util.concurrent.ConcurrentHashMap)
(quick-bench
(let [concurrency-level (.availableProcessors (Runtime/getRuntime))
m (ConcurrentHashMap. (quot (count data) 2) 0.75 concurrency-level)
combinef (fn ([] m) ([_ _]))  ; just return `m` from the combine step
rf (fn [^Map m k]
(let [^AtomicInteger v (or (.get m k) (.putIfAbsent m k (AtomicInteger. 1)))]
(when v (.incrementAndGet v))
m))
reducef ((comp (keep :samplevalue) (map int)) rf)]
(r/fold combinef reducef data)
(into {} m)))
;; My execution time mean: 70 ms

这里我们使用clojure.core.reducers库中的fold来实现并行性。请注意,在并行上下文中,使用的任何转换器都需要是无状态的。还要注意,ConcurrentHashMap不支持使用nil作为键或值;幸运的是,我们不需要在这里这么做。

输出在最后被转换为一个不可变的Clojure散列映射。您可以删除该步骤,只需使用ConcurrentHashMap实例进行额外的加速——在我的机器上,删除into步骤会使整个fold花费大约26ms。

Edit 2017-11-20:User@clojuremostly正确地指出,这个答案的早期版本在初始化并发哈希映射实例的let块中调用了quick-bench,这意味着基准测试在所有运行中都使用相同的实例。我将对quick-bench的调用移动到let块之外。它对结果没有显著影响。

最新更新