インタフェースとしては, そのオブジェクトは, methodとして, 「
が求まったらそれをlistとして返す」というmethod:
と, 「求まった(sync-get!s)
を持っている.(sync-put!s v)
sync-get!は,
synt-put!が実行されるま
で値を返さない. このオブジェクトの実現はABCL/fの特徴を活かした記
述を行なっており, 詳細は5.6.6 (2)節で触れる.
さて, そのようなオブジェクトの記述(cky-matrix-element クラス)が
あったとする. 実際にアルゴリズムを実行する前に,
cky-matrix-elementオブジェクトを各(i,j)の組に対して生成し, 2
次元配列として格納する. 配列はグローバル変数の*cky-matrix*によっ
てアクセスされる(すなわち, 配列そのものは各プロセッサにコピーされる.
そしてその要素であるオブジェクトは先に述べた方法(D(i,j))で分散配置さ
れる). なお, この配列はあらかじめ可能な入力文の最大の長さの分だけ確保
しておき, 各文ごとに作り直すことはしない.
本体の記述は,「各k (i < k < j)に対して,
と
を
sync-get!により取り寄せ, それをもとに
を求め, それら
を全てmergeして, 求まった値をsync-get! する」というメソッド(※)
を書いて, それらを, i,jの全ての組合せに対して並列に(futureという
primitiveを使って)実行する.
まず全てのkに対する処理を逐次的に行なう方式を示す. 先に述べたように,
kを(i + j)/2に近いものから順に求めていくべきであり, 以下では
k0およびk1という二つのパラメータを用意して, それぞれルー
プ中でincrement/decrementしていく.
;; Si,j を求めるメソッド (n は入力文の長さ)
;; ただし, 各 Ti,k,j (i < k < j)を順に行なう
(defmethod cky-matrix-element non-leaf-process (i j n)
(declare (fixnum i j n) (reply-type unit))
(let* ((sij '()))
(declare ((list fixnum) pij))
;; foreach k s.t. (i $<$ k $<$ j)
;; k1は (i + j)/2 から1ずつ増やす.
;; k0は (i + j)/2 -1 から1 ずつ減らす.
(do ((k1 (/ (+ i j) 2) (+ k1 1))
(k0 (- (/ (+ i j) 2) 1) (- k0 1)))
((= k1 j))
;; Si,k と Sk,j が求まるのを待って取り寄せる.
(let* ((sik (sync-get! (mref *cky-matrix* i k1 n)))
(skj (sync-get! (mref *cky-matrix* k1 j n))))
;; Ti,k,j を求めて Si,j に マージする
(setq sij (apply-binary-rules sik skj k1 sij gen-edges)))
(when (> k0 i)
(let* ((sik (sync-get! (mref *cky-matrix* i k0 n)))
(skj (sync-get! (mref *cky-matrix* k0 j n))))
;; Ti,k,j を求めて Si,j に マージする
(setq sij (apply-binary-rules sik skj k0 sij gen-edges)))))
;; Si,j が求まった(待ってる人たちに通知)
(sync-put! self sij)))
図5.7: Si,jを求めるメソッド.
Si,jを表すクラス cky-matrix-element に対する
メソッドとして実現されている.
上で, パラメータi, jは, 求めるべき
のi, jを与えている.
パラメータnは入力文の長さで, 配列*cky-matrix*にアクセス
するために必要である. マクロ:
(mref *cky-matrix* i j n)によって,
(sync-get! (mref *cky-matrix* i k n))および
(sync-get! (mref *cky-matrix* k j n))により,
それぞれ apply-binary-rulesの副作用として更新される.
これができれば後は, 全てのi, jに対してこれらをfutureを用いて
起動すれば良い. 以下のプログラムでは, タスクを生成する側のボトルネック
を避けるために, j - i = gとなるものに対して
を求める処理を起
動する, (cky-parse-non-leaves-line g n)という副関数
を書いて, 各g (
)に対して並列に起動するという
2段のタスク生成を行なっている.
またcky-parse-leavesは, j - i = 1なる
を求める処理を全て並列に起動する副関数である.
(defun cky-parse (sentence)
(declare ((list fixnum) sentence) (reply-type unit))
(let* ((n (length sentence)))
;; Si-1,i を全て求める処理を
(future (cky-parse-leaves sentence n) :on (random-pe))
;; 上と並行に, Si,i+g, を全て求める処理を, 各gに対して
;; 並列に起動する
(do ((g 2 (+ g 1)))
((>= g n))
(future (cky-parse-non-leaves-line g n) :on (random-pe)))
;; 最後に, S0,n を求めて, 終了をここで待つ.
(non-leaf-process (mref *cky-matrix* 0 n n) 0 n n)))
(cky-parse-non-leaves-line g n)の定義は以下のようになっている.
(defun cky-parse-non-leaves-line (g n)
(declare (fixnum g n) (reply-type unit))
;; 各 iの組に対し, Si,i+g を求める処理を並列に起動する
(do* ((i 0 (+ i 1))
(j (+ i g) (+ j 1)))
((> j n))
(future (non-leaf-process (mref *cky-matrix* i j n) i j n))))
メインルーチンcky-parseにおいて, method起動の返事は, 一般には, このようにして, method起動の返事に対してどこでも 終了待ち(touch)をしないのは危険なプログラミングスタイルで, 特に理由がない限り, 全ての method起動に対してどこかでtouchを行なうべきである. 今回は, 正しいことが簡単に証明でき, かつ効率改善に寄与することから採用した.