[ 目次, 前節, 次節, 索引 ]

5.3.1 ベクタとしてのメモリー



通常の計算機のメモリーは, それぞれが小量の情報を含み得る小さな箱の配列と考えられる. 各箱は アドレス (address)とか, 記憶場所 (location)という一意の名前を持っている. 代表的なメモリーシステムには, 二つの基本的演算: 指定した場所に格納したデータを取り出すものと, 指定した場所に新しいデータを代入するものが用意してある. 一群の小さな箱に順にアクセスするため, メモリーアドレスは増やせるようになっている. 更に一般的には, 多くの重要なデータ演算は, メモリーアドレスを, 記憶場所に格納出来たり, 計算機のレジスタで操作したりするデータとして扱えるよう要求する. リスト構造の表現は, そういう アドレス演算(address arithmetic)の一つの応用である.

   計算機メモリーをモデル化するため, ベクタ(vector)という新しい種類のデータ構造を使う. 抽象的にはベクタは合成データオブジェクトである. 各要素は整数の添字を使い, 添字とは独立の時間でアクセス出来る. 5 メモリー演算を記述するには, ベクタを操作するSchemeの基本的手続きを二つ使う:

(vector-refvector⟩ ⟨n)はベクタのn番目の要素を返す.

(vector-set!vector⟩ ⟨n⟩ ⟨value) はベクタのn番目の要素を, 指定した値に設定する.

例えば, vをベクタとして, (vector-ref v 5)はベクタvの5番目の項を取り出し, (vector-set! v 5 7)はベクタvの5番目の項の値を7に変更する.6 計算機メモリーでは, このアクセスは, メモリーでのベクタの先頭番地を規定するベースアドレス(base address)とベクタ中の特定の要素の距離を規定する添字 (index)を組み合せるアドレス演算を使って実装出来る.

Lispデータの表現
ベクタを使ってリスト構造メモリーに必要な基本的な対構造を実装することが出来る. 計算機メモリーが二つのベクタ: the-cars the-cdrsに分割されていると考える. リスト構造を次のように表現する: 対へのポインタは, 二つのベクタへの添字である. 対のcarは指示した添字を持つthe-carsの項目であり, 対のcdrは指示した添字を持つthe- cdrs の項目である. (整数や記号のような)対ではないオブジェクトの表現と, データの種類を区別する方法も必要で, これを実装する多くの方法があるが, そのすべては 型つきポインタ(typed pointers)に簡約出来る. つまり「ポインタ」の考えをデータ型の情報を含むように拡張する.7 データ型により, システムは(「対」データ型と, メモリーベクタへの添字からなる)対へのポインタと, (他のデータ型と, その型のデータを表現するのに使われているものからなる)他の種類のデータへのポインタを区別出来る. 二つのデータオブジェクトは, ポインタが一致すれば 同じ(eq?)と考える.8 図5.14はこの方法の使ったリスト((1 2) 3 4)の表現を示している. 箱とポインタ図も示す. データ型の情報を示すのに, 英字を先づけする. そこで添字5の対へのポインタはp5 で示し, 空リストはポインタe0で示し, 整数4へのポインタはn4で示す. 箱とポインタ図では, 対の左下に, 対のcarcdrが格納されている場所を指定するベクタの添字を示した. the-carsthe-cdrs の空白の場所は, (ここには示さない)他のリスト構造の部分が入っているであろう.



図5.14 リスト((1 2) 3 4)の箱とポインタ表現とメモリーベクタ表現

   n4のような整数へのポインタは, 数値データを示す型と, 整数4の実際の表現とからなるであろう.9 ポインタ一個に割り当てられた固定の大きさのスペースで表記するには大き過ぎる整数を扱うには, 別の ビッグナム(bignum)データ型を使う. この型では, ポインタは整数の各部分を格納するリストを指定している.10

   記号は, 記号の印字表現を形成する文字の並びを指示する型つきポインタで表す. この並びは文字列が入力に最初に現れた時に, Lispの読込みルーチンが構成する. 一つの記号の二つの実体が eq?で「同じ」記号として認めて貰いたいし, eq?をポインタの等価の単純なテストとしたいので, 読込みルーチンが同じ文字列を二度見た時, その二度の出現に(同じ文字の並びへの)同じポインタを使うよう保証しなければならない. これを実現するため, 読込みルーチンは, 伝統的に オブアレイ(obarray)という, それまでに出会ったすべての記号の表を保持する. 読込みルーチンが文字列を見つけ, 記号を構成しようとする時, 同じ文字列を以前に見たことがあるか知るため, オブアレイを調べる. まだ見たことがなければ, 文字列を使って新しい記号(新しい文字列への型つきポインタ)を構成し, このポインタをオブアレイに入れる. 読込みルーチンがこの文字列を以前に見たことがあれば, オブアレイに格納されている記号のポインタを返す. この文字列を一意的ポインタに取り替える処理を, 記号の インターン(interning)[閉じ込めるの意]という.

基本リスト演算の実装
上述の表現法があれば, レジスタ計算機の「基本的」リスト演算のそれぞれを一個かそれを超える数の基本ベクタ演算で取り替えることが出来る. メモリーベクタを指す二つのレジスタ, the-carsthe-cdrsを使い, 基本演算としてvector-refvector-set!が使えると仮定する. また(ポインタを増やすとか, 対のポインタをベクタの添字つけに使うとか, 二つの整数を足すとかのような)ポインタの算術演算は, 型つきポインタの, 添字の部分だけを使うと仮定する.

例えば, 命令

(assign ⟨reg1⟩  (op car) (reg ⟨reg2⟩))

(assign ⟨reg1⟩  (op cdr) (reg ⟨reg2⟩))
は, これらのそれぞれを
(assign ⟨reg1⟩ (op vector-ref) (reg the-cars) (reg ⟨reg2⟩))

(assign ⟨reg1⟩ (op vector-ref) (reg the-cdrs) (reg ⟨reg2⟩))
と実装すればレジスタ計算機で実現出来る. 命令
(perform (op set-car!) (reg ⟨reg1⟩) (reg ⟨reg2⟩))

(perform (op set-cdr!) (reg ⟨reg1⟩) (reg ⟨reg2⟩))
(perform
 (op vector-set!) (reg the-cars) (reg ⟨reg1⟩) (reg ⟨reg2⟩))

(perform
 (op vector-set!) (reg the-cdrs) (reg ⟨reg1⟩) (reg ⟨reg2⟩))
と実装する.

   consは未使用の添字を割り当て, consの引数をthe-carsthe-cdrsのその添字の指すベクタの位置に格納して実行する. 特別なレジスタ freeがあって, 次に使用可能な添字を持つ対へのポインタを保持しており, 次の空き場所を見つけるために, そのポインタの添字部分を増やすことが出来ると仮定する.11 例えば命令

(assign ⟨reg1⟩ (op cons) (reg ⟨reg2⟩) (reg ⟨reg3⟩))
は, 次の一連のベクタ演算で実装する.12
(perform
 (op vector-set!) (reg the-cars) (reg free) (reg ⟨reg2⟩))
(perform
 (op vector-set!) (reg the-cdrs) (reg free) (reg ⟨reg3⟩))
(assign ⟨reg1⟩ (reg free))
(assign free (op +) (reg free) (const 1))
eq?演算
(op eq?) (reg ⟨reg1⟩) (reg ⟨reg2⟩)
はレジスタの全フィールドの等価をテストし, pair?, null?, symbol?およびmember?のような述語は, 型フィールドを調べるだけが必要である.
スタックの実装
われわれのレジスタ計算機は, スタックを使うが, スタックはリストを使いモデル化出来るので, ここでは何も特別なことをする必要はない. スタックは特別なレジスタthe-stackの指す, 退避した値のリストとなし得る. 従って(savereg)
(assign the-stack (op cons) (reg ⟨reg⟩) (reg the-stack))
と実現出来る. 同様に(restorereg)
(assign ⟨reg⟩ (op car) (reg the-stack))
(assign the-stack (op cdr) (reg the-stack))
と実現出来, また(perform (op initialize-stack))
(assign the-stack (const ()))
と実現出来る. これらの演算は上述のベクタ演算を使って更に展開出来る. しかし通常の計算機アーキテクチャでは, スタックは別のベクタに割り当てるのが得策である. そうすればスタックのプッシュとポップは, そのベクタへの添字の増減で実現出来る.

問題 5.20


(define x (cons 1 2))
(define y (list x x))
で作られたリスト構造の箱とポインタ表記およびfreeポインタは最初にp1 であるとして, (図5.14のような)メモリーベクタ表現を描け. freeの最後の値は何か. どのポインタがxyの値を表しているか.

問題 5.21


次の手続きのレジスタ計算機を実装せよ. リスト構造用のメモリー演算は, 計算機の基本演算として使用可能とする.

a. 再帰的count-leaves:
(define (count-leaves tree)
  (cond ((null? tree) 0)
        ((not (pair? tree)) 1)
        (else (+ (count-leaves (car tree))
                 (count-leaves (cdr tree))))))
b. カウンタを陽に持つ再帰的count-leaves:
(define (count-leaves tree)
  (define (count-iter tree n)
    (cond ((null? tree) n)
          ((not (pair? tree)) (+ n 1))
          (else (count-iter (cdr tree)
                            (count-iter (car tree) n)))))
  (count-iter tree 0))


問題 5.22


3.3.1節の問題3.12は, 二つのリストを連接して新しいリストを形成するappend手続きと, 二つのリストを貼り合せるappend!手続きを示した. これらの手続きのそれぞれを実装するレジスタ計算機を設計せよ. リスト構造用メモリー演算は基本演算として使用可能とする.


5 メモリーを項目のリストで表現することも出来る. しかしリストのn番目の要素のアクセスは, n - 1回のcdr演算を必要とするので, アクセス時間は添字とは独立でない.

6 完全のためには, ベクタを構成する make-vector演算を規定しなければならない. しかし今の応用では, ベクタは計算機メモリーの固定部分をモデル化するだけに使う.

7 これは2章で, 汎用演算を扱った時に紹介した 「タグつきデータ」の考え方と全く同じである. しかしここでは, データ型は, リストを使って構成したのではなく, 基本的な機械レベルで組み込まれている.

8 型情報は, Lispシステムを実装する計算機の細部に従い, いろいろな方法でコード化される. Lispプログラムの実行効率は, これをいかに賢明に選択したかに強く依存するが, うまい選択の一般的設計規則を形式化するのは難しい. 型つきポインタの最も直截な方法は, 各ポインタの決ったビットの位置を, データ型をコード化する 型フィールド(type field)にしてしまうことである. こういう表現を設計する時に考えるべき重要な問題には, 次のものがある: つまり型に何ビットが必要か, ベクタの添字は最大どのくらい必要か. 基本的計算機命令で, ポインタの型フィールドがどの位効率よく操作出来るか, など. 型フィールドが効率よく扱える特別なハードウェアを持つ計算機は タグアーキテクチャ(tagged architecture)を持つという.

9 数の表現のこの決定は, ポインタの等価をテストするeq?が整数の等価に使えるかどうかを決定する. ポインタが数自身を含むなら, 等価の数は同じポインタを持つであろう. しかしポインタが数の格納してある場所の添字を持つなら, 同じ整数を複数の場所に格納しないように注意した時だけ, 等しい数が等しいポインタを持つことを保証する.

10 これは整数を数字の並びとして書くようなものである. ただし各「数字」は0から一個のポインタとして格納出来る最大数までの間の整数である.

11 空き場所を見つける別な方法もある. 例えば未使用の対のすべてを フリーリスト(free list)につなぐことが出来る. 5.3.2節で見るように, 詰込み式のごみ集めを利用するので, われわれの空き場所は連続的である. (従ってポインタを増やすことでアクセス出来る.)

12 これは本質的に3.3.1節に述べたようにset-car!set-cdr!を使ってのconsの実装である. あの実装で使った演算get-new-pairはここではfreeポインタで実現している.

[ 目次, 前節, 次節, 索引 ]