巨大なベクタやリストのmapなどはシングルコアで動作させるにはもったいないが、手動で並列化を行うのは骨が折れる作業だ。 速度面でのボトルネックになっている部分を高速化できれば,効率的に処理を行うことができる.
lparallelは並列・並行処理に関する関数やマクロを提供する。
この記事ではpmap
とpreduce
について紹介する.
例: pmap
: 並列なmap
pmapは、コレクションをいくつかに分割し、各ワーカが並列にmapを行った後で、それらのコレクションが元の順序に結合される。
(ql:quickload :lparallel) (setf lparallel:*kernel* (lparallel:make-kernel 2)) ; ワーカを2並列で初期化する (map 'list #'(lambda (x) (* 2 x)) '(1 2 3 4 5 6 7 8 9)) ; 通常のmap (pmap 'list #'(lambda (x) (* 2 x)) '(1 2 3 4 5 6 7 8 9)) ; 並列map
lparallelの使い方は、基本的に
- kernelの初期化を行う
- lparallelに含まれる関数を呼び出す
となる。kernelの初期化を行わずに並列操作関数を呼び出すと、デバッガに落ちてkernelを対話的に初期化できる。
他にもpreduce
やdefpun
などの並列な関数が用意されている。
例: preduce
: 並列なreduce
preduce
は,reduce
を並列化したものだ.これは2つの段階に分けて計算される.
- コレクションを分割し,個々に
reduce
適用を並列して行う.コレクションはそれぞれ値に変換され,値のコレクションができる. - 値のコレクションに対して
reduce
適用が行われ,最終的な値が定まる.
したがって,preduceは二項演算が適用される順序がreduceとは違うので,preduceには結合的(associative)な関数を渡す必要がある. #'+
は結合的なのでpreduceでもreduceと同じように動くが,#'-
は結合的ではないから同じように動作しない.
CL-USER> (reduce #'+ '(1 2 3 4 5 6 7 8 9 10)) 55 CL-USER> (lparallel:preduce #'+ '(1 2 3 4 5 6 7 8 9 10)) 55 CL-USER> (reduce #'- '(1 2 3 4 5 6 7 8 9 10)) -53 CL-USER> (lparallel:preduce #'- '(1 2 3 4 5 6 7 8 9 10)) 11
+は結果が同じだが-は異なる結果を返す.
標準のreduce
ができるように,preduce
もschemeにおけるfold
相当の操作を:initial-value
で行うことができる.
ただし,この値が使用されるのは最初のステップでコレクションを分割したときであり,部分ごとに使われるということに注意しなければならない.
CL-USER> (lparallel:preduce #'* '(1 2 3 4) :parts 1 :initial-value 10) ; '((10 1 2 3 4)) 240 CL-USER> (lparallel:preduce #'* '(1 2 3 4) :parts 2 :initial-value 10) ; '((10 1 2) (10 3 4)) 2400 CL-USER> (lparallel:preduce #'* '(1 2 3 4) :parts 3 :initial-value 10) ; '((10 1) (10 2) (10 3 4)) 24000 CL-USER> (lparallel:preduce #'* '(1 2 3 4) :parts 4 :initial-value 10) ; '((10 1) (10 2) (10 3) (10 4)) 240000
ちなみに:recurse
オプションをt
にすると,第2ステップのreduce
適用がpreduce
に変わり,一貫してpreduce
を使うようになる.
まとめ
今回はpmap
/preduce
を紹介した.lparallel
にはこれらに限らずpromiseやptreeなどの便利な機能が用意されているので,また利用機会があればメモを残そうと思う.