(Answer credit: Renzo Borgatti (@reborg).)
首先,让我们设置一些示例数据,稍后我们将使用这些数据进行性能测试。该向量包含 500k 个具有相同键的映射。值重叠 1/5 的时间。
(def data
(mapv hash-map
(repeat :samplevalue)
(concat (range 1e5)
(range 1e5)
(range 1e5)
(range 1e5)
(range 1e5))))
现在让我们使用传感器进行转换。请注意,此解决方案不是并行的。我将你缩短.intValue
为 just int
,它做同样的事情。此外,:samplevalue
从每个地图有条件地获取可以缩短为 just (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
我们所做的一样,我们已经在瞬态哈希图上完成了操作,以获得额外的性能。我们通过使用瞬态哈希图作为种子和completing
调用persistent!
它的最终值来做到这一点。
我们可以使这个平行。为了获得最佳性能,我们使用可变的 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
这里我们使用fold
fromclojure.core.reducers
库来实现并行。请注意,在并行上下文中,使用的任何转换器都需要是无状态的。另请注意, aConcurrentHashMap
不支持nil
用作键或值;幸运的是,我们不需要在这里这样做。
最后将输出转换为不可变的 Clojure 哈希图。您可以删除该步骤并仅使用 ConcurrentHashMap 实例来获得额外的加速 - 在我的机器上,删除该into
步骤使整个过程fold
大约需要 26 毫秒。
编辑 2017-11-20:用户 @clojuremostly 正确地指出,此答案的早期版本调用了初始化并发哈希映射实例quick-bench
的块内部let
,这意味着基准在所有运行中都使用了相同的实例。我把电话移到quick-bench
了let
街区外。它对结果没有显着影响。