Upload
shohei-murayama
View
303
Download
0
Embed Size (px)
Citation preview
実装
• BankersDqeueはAmortized Boundなデータ構造であるので、7章で学習した手順でWorst-Case Boundなデータ構造へと変換していきます。
1. Monolithic関数をIncremental関数に変換します
2. Dequeの各操作ごとにサスペンションを少しずつ実行するスケジュールを導入します
BankersDequeで使用したmonolithic関数
BankersDqueでは、フロントとリアの2つのStreamを、以下のように操作してローテーションを行っていました。
1. 長い方のStreamを全体の半分の長さだけ残す – 残す方をtakeで取得 – 半分を超える部分をdropで作成
2. 半分を超える部分をreverseして短い方のstreamと結合 (これで2つのstreamが同じ長さになる) – 半分を超える部分をreverse – 短い方のリストを前にしてreverseした結果と++
Queueの場合はリアを空にしてフロントにすべて寄せていましたが、Dequeではフロントとリアの長さを同じにします。
|f| < |r| の場合:
val r’ = take (j, r) val f’ = f ++ reverse (drop (j, r))
ここに登場したtake/drop/reverseの各関数は、4章で定義されたものです。
Streamの実装(参考)
P36 の Figure 4.1 より
fun lazy ($NIL) ++ t = t | ($CONS (x, s)) ++ t = $CONS (x, s ++ t)
fun lazy take (0, s) = $NIL | take (n, $NIL) = $NIL | take (n, $CONS (x, s)) = $CONS (x, take (n-1, s))
fun lazy drop (n, s) = let fun drop’ (0, s) = s | drop’ (n, $NIL) = $NIL | drop’ (n, $CONS (x, s)) = drop’ (n-1, s) in drop’ (n, s) end fun lazy reverse s = let fun reverse’ ($NIL, r) = r | reverse’ ($CONS (x, s), r) = reverse’ (s, $CONS (x, r)) in reverse’ (s, $NIL) end
サスペンションを1つ潰すが、 新しいStreamのために構築したサスペンションで、後続の処理を遅延させる
既存のサスペンションをすべて潰しながら、 新たなStreamを終端から構築する
incremental関数
monolithic関数
monolithic関数からincremental関数へ
BankersDequeでのStream操作を分類します。 – Incremental関数
• take
• ++
– monolithic関数
• reverse
• drop
以上から、RealTimeDeque専用のdropとreverseを用意します。 – rotateDrop
– rotateRev
rotateDrop / rotateRevの戦略
• Incremenetal関数への変換
– Streamに対する処理を分割して実行し、毎回一定数(𝑐 > 1)ずつ処理を行うようにします。 ⇒ 一定数ずつ処理することで、1回の処理時間がO(1)になる。
• スケジュールの導入
– ローテーションの結果として新規作成されるStreamのデータ構築子$CONSを
サスペンションとして、その内側に関数の呼び出しを配置して実行を遅延します。
– Sectio7.2のRealTimeQueueと同様に、 Streamを先頭から順に開いていくことでサスペンションを潰していく、スケジュール機構を構築します。
rotateDrop
1実行ごとに c ずつdropします。rotateDropの各回の呼び出しは、短い方のStreamを再作成する$CONSの内側に配置します。
fun rotateDrop (f, j, r)= if j < c then rotateRev (f, drop (j, r), $NIL) ① else let val ($CONS (x, f’)) = f in $CONS (x, rotateDrop (f’, j – c, drop (c, r))) end ②
①
②
c=3 の場合
rotateRev
1実行ごとに c ずつreverseします。rotateDropの各回の呼び出しは、短い方のStreamを再作成する$CONSの内側に配置します。
fun rotateRev ($NIL, r, a) = reverse r ++ a | rotateRev ($CONS (x, f), r, a) = $CONS (x, rotateRev (f, drop (c, r), reverse (take (c, r)) ++ a))
c=3 の場合
ローテーション後のフロントとリア
新たに作成されたStreamに内包されて処理が遅延しています。
サスペンションはフロントとリアの両方に作成されます。
rotateDrop が作成
rotateRev が作成
++ が作成 take が作成
フロント リア
drop rotateDrop
drop take
reverse ++
rotateRev
++ take
Streamの$CONSで遅延している処理
++ や take 自身が再帰して作成するので、他の処理を内包できない (専用のを作れば別だろうけど)
スケジュールの導入
ローテーション実行中にrotateDrop関数と rotateRev関数が作成したサスペンションを、次回のローテーション開始までに全て実行しなければなりません。
このためにスケジュールを導入します。
type ‘a queue = int * ‘a Stream * ‘a Stream (* front *) * int * ‘a Stream * ‘a Stream (* rear *)
ローテーションで新しいフロントとリアができるので、そのままスケジュールとします。
let j = (lenf + lenr) / 2; let i = lenf + lenr –j; let r' = S.take (j, r); let f' = rotateDrop (f, j, r) in (i, f', f', j, r', r')
Streamを順に開いていくことでサスペンションを実行します。
fun exec1 ($CONS (x, s)) = s | exec1 s = s
スケジュール データ データサイズ
解析
短い方のStreamもとに作成する$CONSで rotateDrop と rotateRev を遅延させることができるように、Balance Invariantを設定します。
• Balance Invariant:
𝑓 ≤ 𝑐 × 𝑟 + 1 𝑟 ≤ 𝑐 × 𝑓 + 1 (BankersDequeと同じ)
• 上記から、ローテーション開始時のフロントとリアの長さ: 短い方 = 𝑚 長い方 = 𝑐 ×𝑚 + 𝑘 + 1 𝑤ℎ𝑒𝑟𝑒 1 ≤ 𝑘 ≤ 𝑐
例) c=3の場合、以下のような状態と操作でローテーションが開始する k=1: |f|=5, |r|=16 のときの snoc、|f|=6, |r|=17 のときの tail k=2: |f|=6, |r|=18 のときの tail k=3: |f|=6, |r|=19 のときの tail
解析
定数 c の範囲を求めます。
• 定数 c の範囲 – 定数 j を長い方から短い方へ移動する長さとする。
– rotateRev 開始時に以下が成立する。 長い方 ≥ 𝑐 × 短い方 + 1 + 𝑘 − 𝑗 𝑚𝑜𝑑 𝑐
– 長い方 ≥ 𝑐 × 短い方 , 1 ≤ 𝑘 ≤ 𝑐, 𝑐 > 1 なので、 1 + 𝑘 − 𝑗 𝑚𝑜𝑑 𝑐 ≥ 0 1 + 1 − 𝑐 − 1 ≥ 0 (k の最小値と j mod c の最大値を代入) 𝑐 ≤ 3
– 𝑐 = 2 または 𝑐 = 3
解析
• ローテーション後のフロントとリアの長さ:
ローテーション前に短かった方 =𝑓 + |𝑟|
2
ローテーション前に長かった方 =𝑓 + |𝑟|
2
• 次回ローテション実行までの最短操作回数
– Init / tail のどちらかを連続実行します(cons / snoc よりも1
𝑐倍の回数ですむ)。
– ローテーション後のフロント、リアの長さをともに n とすると
𝑐 × 𝑛 −最短操作回数 + 2 ≤ 𝑛
𝑛 −最短操作回数 ≤𝑛 − 2
𝑐
最短操作回数 ≥ 𝑛 −𝑛 − 2
𝑐=𝑛 + 2
𝑐>𝑛
𝑐
𝑛+2
𝑐の項が最大になるのは𝑐 = 2のときで、init / tail を最短で約
𝑛
2回実行すると、次のロー
テーションが発生します。
Exercise 8.7
• ローテーションで作成されるサスペンション数は、フロントとリアともにStreamの長さと一致します。
• Debits Invariantを以下のように設定します。 – ローテーション開始時の 𝑐 × 短い方 + 1+ 𝑘 ≥ 長い方 𝑤ℎ𝑒𝑟𝑒 1 ≤ 𝑘 ≤ 𝑐 から、
一方の𝑆𝑡𝑟𝑒𝑎𝑚の未評価サスペンション数 ≤ 𝑐 × 短い方 + 2− 長い方 さらに次回ローテション実行までの最短操作回数の考察から c = 2 を代入して、 一方のStreamの未評価サスペンション数 ≤ 2 × 短い方 + 2− 長い方
• 以上から – ローテーション実行直後、短い方と長い方の長さは高々1違うだけなので、Debits Invariantを満たしている。
– Dequeから削除するとき、短い方が1短くなっても、2つのStreamのいずれも、サスペンションを2減らせばDebits Invariantを維持できる。
– Dequeに追加するとき、長い方が1短くなっても、2つのStreamのいずれも、サスペンションを1減らせばDebits Invariantを維持できる。
おまけ: RealTimeDequeの動作例
例) Dequeの操作とローテーション
1. c = 3, |f| = 3, |r| = 9 の RealTimeDeque q1 に対して • snoc q1 x ⇒ |f| = 3, |r| = 10
• snoc (snoc q1 x) y ⇒ |f| = 3, |r| = 11 ⇒ ローテーション ⇒ |f| = 7, |r| = 7
2. c=3, |f| = 7, |r| = 18 の RealTimeDeque q2 に対して • tail q2 ⇒ |f| = 6, |r| = 18
• tail (tail q2) ⇒ |f| = 5, |r| = 18 ⇒ ローテーション ⇒ |f| = 12, |r| = 11
OCamlによるサンプルコード: https://gist.github.com/3271021
おまけ: Global Rebuildingとの違い
だいたいこんな感じだと思います。
• Global Rebuildingには必要だったローテーション用のコピーが、Lazy Rebuildingでは不要。
• Global Rebuildingを使用したHood MelvilleQueueでは、ローテーションに必要な処理が比較的均等に分散していたが、Lazy Rebuildingを使用したRealTimeDequeでは、短い方のStreamを再作成する$CONSだけを使ってdropやreverse遅延させているので、サスペンションの実行コストに偏りがある。