【问题标题】:atom is slow when using it with big mapatom 与大地图一起使用时速度很慢
【发布时间】:2015-01-15 13:47:55
【问题描述】:

我有一个使用 clojure 的 ETL,每个线程可以加载文件的不同部分,并且还需要从业务密钥中获取密钥。存储业务键到键映射的数据结构是一个哈希映射,例如:

{"businessKey1" 1, 
 "businessKey2" 2,
 "businessKey3" 3, 
 "businessKey4" 4, 
 "businessKey5" 5 }

ETL从文件中加载数据时,会将文件中的每一行解析成列,如果在map中可以找到业务key列,则返回key,例如找到businessKey1,则返回1。找到businessKey6,然后需要调用一个Web服务来创建一个新的密钥。我打算用atom,所以当每个线程找到一个新的key时,用atom来修改map。但性能非常糟糕。我测试了下面的代码,速度很慢,而且有很多GC活动。

(def a (atom {}))
(map #(swap! a (partial merge {% 1})) (range 10000))
(println a)

对此最好的解决方案是什么?我应该在 java 中使用 ConcurrentHashMap 吗?

【问题讨论】:

  • “非常慢”是什么意思?由于地图是不可变的数据结构,很多 GC 活动是可以理解的,因为代码在每个 merge 上创建一个新地图。也就是说,GC 可能不是瓶颈的根源,因为有一个原子正在被修改并且需要同步。

标签: clojure


【解决方案1】:

性能不佳的主要来源似乎是(partial merge {% 1})的使用

更惯用的形式如下:

(let [a (atom {})] 
  (doall (map #(swap! a merge {% 1}) (range 10000))) (println @a)))

更快的是使用assoc而不是每次都创建临时地图:

(let [a (atom {})] 
  (doall (map #(swap! a assoc % 1) (range 10000))) (println @a)))

如果你想迭代一个 seq 的副作用,最好使用doseq

(count (let [a (atom {})] (doseq [r (range 10000)] (swap! a assoc r 1))))

原子不是必需的,您想要的可以表示为减少:

(count (reduce (fn [m r] (assoc m r 1)) {} (range 10000)))

【讨论】:

  • 不错的收获! OP 提到在 ETL 代码中使用多个线程,因此需要某种同步,使用原子或其他类型的 Clojure 引用类型。
【解决方案2】:

您可以通过使用 Clojure reducers 来避免在此处使用原子:

(require '[clojure.core.reducers :as r])

(defn lookup [k]
  ; do remote call, here it just returns 1  
  1)

(defn f
  ([] {})
  ([acc k] (if (get acc k)
             acc
            (assoc acc k (lookup k)))))

(r/fold merge f (vec (range 10000)))

clojure.core.reducers/fold 将自动并行运行并合并结果。

【讨论】:

  • 使用fold 是错误的,因为它是并行运行的。对于同一个键 lookup 可能会被调用两次,因此会创建两个不同的键。
  • 上述使用 fold 的方案并没有错。是的,如果集合多次包含相同的键并且折叠集合的方式使得相同的键出现在不同的分区中,那么将使用相同的键多次调用查找。远程 Web 服务应该已经为创建密钥调用提供服务,该调用包含可能已经存在的密钥,因为它可能有多个并发远程客户端,并且无法保证任何客户端都不会尝试使用现有密钥。 Web 服务的 API 契约应该定义如何处理这种情况。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-03-26
  • 1970-01-01
  • 2018-12-08
  • 1970-01-01
  • 1970-01-01
  • 2022-12-22
相关资源
最近更新 更多