Pretty printer を支える技術。
Common Lispは言語仕様にPretty Printerが含まれます。
Pretty Printer
とはものすごく大雑把にいうとコードフォーマッタです。
Common Lispは言語仕様にコードフォーマッタが含まれる世にも珍しい言語なのです(?)。
XP.
Common Lispは名前が示すとおり、すでに存在している複数のLisp処理系間で可搬的にコードを書けるように一大統一定義を作成しようとして出来上がったものです。
それまでは各処理系が独自にPretty Printer
を持っていたのですが、それらは処理系内部のものでユーザに開放されてはおりませんでした。
そこで拡張可能なPretty Printer
として提案されたのがXP
です。
(おそらくeXtensible Printerの略記かと思われるのですが、詳細は不明です。)
このたび、XP
のコードが発掘されgithubに公開されていたので興味本位でフォークして弄り倒しました。
その結果最適化等、大変興味深い発見が複数あったのでここに記します。
Adjustable vector.
Common LispはAdjustable Array
をサポートしている言語です。
Adjustable Array
とは可変長配列のことです。
要素の追加にはVECTOR-PUSH-EXTEND
を使います。
* (defvar aa (make-array 1 :adjustable t :fill-pointer 0))
AA
* aa
#()
* (vector-push-extend :a aa)
0
* aa
#(:A)
List
へのPUSH
が先頭から行われるのと異なり、Adjustable Array
へのVECTOR-PUSH-EXTEND
は末尾へ要素を追加します。
* (vector-push-extend :b aa)
1
* aa
#(:A :B)
すでに上に見たとおり、要素数がいっぱいになった場合、処理系が背後でいい具合に長さを拡張してくれます。
変数AA
に束縛されたAdjustable Array
は最初長さ1
で定義されましたが、今では長さが2
に拡張されています。
大変便利なものですが、トレードオフとして計算量の増加が挙げられます。
XP
では最適化のため、Adjustable Array
を模した配列操作が行われています。
POSITION
やFILL
といった各種シーケンス関数からAREF
といった配列操作関数まで、Adjustableな配列より通常の配列の方が処理系はすばやく処理できます。
拡張に関する戦略はHASH-TABLE
のそれに似ていて、入力が最大長を超えると、ある程度の長さをまとめて確保し以後はその新しい配列を参照するというものです。
欠点としては、要素を追加する際は必ず長さチェックのコードを人間が書いておかなくてはならない点です。
美点としては、どこでチェックするかを制御できる点です。
ある関数が配列への入力処理を複数回行う場合、最終的に必要となる長さを求め、冒頭でチェックを一度すればそれで事足ります。
このような細やかな制御は自動化されていないからこそできることです。
Vector queue.
わかりやすさのためにRGB
によるビットマップ画像を例として挙げましょう。
配列にRGB
が入っていますが、これを各RGB
値をメンバ変数に持つcolor
オブジェクトとする場合、コストが大変高くつきます。
オブジェクトの作成(コンストラクト/new/アロケート)は割と高く付く操作だからです。
そこでこれを二次元配列として、各行をcolor
とみなします。
こうすればcolor
オブジェクトを作る必要はありません。
各RGB
値には配列へのインデックスで直接アクセスします。
先に見た通り、XP
は一次元可変長配列を持っています。
そこでXP
では、二次元配列ではなく一次元配列でこれを管理します。
一次元配列にRGBRGBRGB...
と並んでいると思ってください。
各color
をループで回していく場合は行数*要素数+オフセット
でアクセスするわけです。
XP
ではqueue
がこのような実装となっています。
Object pooling.
先にも記した通り、オブジェクトの作成は高価です。
そこでXP
では一度作成したオブジェクトを破棄せずプールして再利用する設計になっています。
XP-STRUCTURE
というSTREAM
的なオブジェクトとcircularityチェックのためのHASH-TABLE
がプーリングされます。
Layered function.
Adjustable Vector
でも記しましたが、XP
ではinner-loop(同じ結果になる計算をループの内側で繰り返し行うこと)を徹底的に避けるように設計されています。
最下層の関数は範囲チェックを行わない危険なもので、可能な限り上層部で一度のチェックで済ます設計になっています。
Identified error.
XP
はCommon Lispが現在の形になる前に書かれたものです。
つまりEXTENDED-LOOP
、CLOS
、CONDITION-SYSTEM
がCommon Lispに含まれる前に書かれたものです。
大変興味深かったのはエラーハンドリングです。
現在ではエラーはその種類によってCONDITION
を定義するのが定石です。
しかしながらXP
では各エラーにID
が振られ、それをTHROW
する作りとなっています。
エラーナンバーでエラーの種類を表すというのはなんとも古風なやり口で訝しんだものですが、少々見直しました。
というのもこのエラーIDがテストの網羅性に一役かっているからです。
CONDITION
の型でエラーの種類を表す場合、残念ながらテストの網羅性には寄与しません。
Conclusion.
泥臭い最適化が多くてしみじみしました。
Discussion