テキスト ファイル内の特定の機能の頻度を同時にカウントし、データを照合する関数があります。関数の出力は、永続的なマップに保存された数千の度数分布です。簡単な例として:
{"dogs" {"great dane" 2, "poodle" 4}, "cats" {"siamese" 1 "tom" 3}}
そして、これを生成するコード:
(defn do-the-thing-1 [lines species_list]
;; we know the full list of species beforehand so to avoid thread contention
;; for a single resource, make an atom for each species
(let [resultdump (reduce #(assoc %1 %2 (atom {})) {} species_list)
line-processor (fn [line]
(fn [] ; return a function that will do the work when invoked
(doseq [[species breed] (extract-pairs line)]
(swap! ; increase the count for this species-breed pair
(resultdump species)
update-in [breed] #(+ 1 (or % 0))))))
pool (Executors/newFixedThreadPool 4)]
;; queue up the tasks
(doseq [future (.invokeAll pool (map line-processor lines))]
(.get future))
(.shutdown pool)
(deref-vals result)))
(defn deref-vals [species_map]
(into {} (for [[species fdist] species_map] [species @fdist]))
これはうまくいきます。問題は、それらを使用する前に確率分布に変換する必要があることです。例えば
{"dogs" {"great dane" 1/3, "poodle" 2/3}, "cats" {"siamese" 1/4, "tom" 3/4}}
これを行う関数は次のとおりです。
(defn freq->prob
"Converts a frequency distribution into a probability distribution"
[fdist]
(let [sum (apply + (vals fdist))]
(persistent!
(reduce
(fn [dist [key val]] (assoc! dist key (/ val sum)))
(transient fdist)
(seq fdist)))))
ディストリビューションが処理パイプラインの次のステップで消費されるときにこの変換をオンザフライで行うと、妥当な速度が得られますが、一部のディストリビューションが複数回使用されるため、かなりの量の冗長な変換も発生します。結果を返す前に変換を並行して実行するように関数を変更すると、処理の後半の段階で発生する速度が劇的に低下します。
変更された関数は次のとおりです。
(defn do-the-thing-2 [lines species_list]
;; we know the full list of species beforehand so to avoid thread contention
;; for a single resource, make an atom for each species
(let [resultdump (reduce #(assoc %1 %2 (atom {})) {} species_list)
line-processor (fn [line]
(fn [] ; return a function that will do the work when invoked
(doseq [[species breed] (extract-pairs line)]
(swap! ; increase the count for this species-breed pair
(resultdump species)
update-in [breed] #(+ 1 (or % 0))))))
pool (Executors/newFixedThreadPool 4)]
;; queue up the tasks
(doseq [future (.invokeAll pool (map line-processor lines))]
(.get future))
;; this is the only bit that has been added
(doseq [future (.invokeAll pool (map
(fn [fdist_atom]
#(reset! fdist_atom (freq->prob @fdist_atom)))
(vals resultdump)))]
(.get future))
(.shutdown pool)
(deref-vals result)))
そうです、これによりfreq->prob
、結果のマップへのすべてのアクセスを単純に呼び出す場合よりも、その後のすべてが約 10 倍遅くなりますが、返されるデータは同じです。それがなぜなのか、またはそれについて私ができることについて、誰かが理由を提案できますか?
編集:私は今、Clojureの分数と関係があるのではないかと疑っています. 分数の代わりに float または double を作成するように関数を変更するとfreq->prob
、確率分布をその場で生成するのではなく、事前に計算するときにパフォーマンスが向上します。原子内で作成された分数は、原子外で作成された分数よりも遅く実行されるのでしょうか? これが当てはまらないことを示すいくつかの簡単なテストを実行しただけなので、ここで何か奇妙なことが起こっていることは間違いありません。