19
8.4.3 Real-Time Deques 2012-08-04 (初版) 2012-08-14 (2版) @yuga

PFDS 8.4.3 Real-Time Deques

Embed Size (px)

Citation preview

8.4.3 Real-Time Deques

2012-08-04 (初版)

2012-08-14 (2版)

@yuga

目次

• 狙い

• 実装

• 解析

• Exercise 8.7

• おまけ

狙い

• すべての操作が最悪実行時間O(1)であるDequeを作ります。

• 8.4.2で登場したBankersDequeを拡張して実現します。

実装

• 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遅延させているので、サスペンションの実行コストに偏りがある。

参考文献

• Chris Okasaki, “8.4.3 Real-Time Deque”, Purely Functional Data Structures, Cambridge University Press (1999)

• Chris Okasaki, “Simple and efficient purely functional queues and deques”, Journal of Functional Programming, 5(4):583-592, October 1995