1. 程式人生 > >5.3.1 作為向量的記憶體

5.3.1 作為向量的記憶體

5.3.1 作為向量的記憶體
傳統的計算機記憶體被視為有許多小房間的陣列,它的任何一間都能包含著一片資訊。
任何一間都有獨特的名稱,叫做它的地址或者是位置。經典的記憶體系統提供了兩個原生的操作:
一個是從特定的位置取得儲存的資料,一個是向特定的位置賦新值。在一定的區域內,
記憶體地址能夠自動增長來支援按順序讀取資料。更一般地說,許多重要的資料操作要求記憶體地址
也被看作是資料,地址也能被存入記憶體,在機器的暫存器中被操作。列表結構的表示
就是這種地址演算法的一種應用。

為了模型化計算機記憶體,我們使用一種新的資料結構,叫做向量。抽象地說,
一個向量是一個複合的資料結構,它的獨立的元素能被通過一個整數的索引
讀取到,在一定的時間內它獨立於索引。為了描述記憶體的操作,我們使用
兩個原生的SCHEME應用程式來操作向量。

(vector-ref <vector> <n>) 返回向量的第N個元素
(vector-set! <vector> <n> <value>) 設定向量的第N個元素的值為V。

例如,如果V是一個向量,然後(vector-ref V 5) 得到在向量V中的第五個元素。
(vector-set! V 5 7) 則是把向量V的第五個元素的值改為7。對於計算機記憶體,
這種讀取被實現通過使用地址演算法。即組合基地址(指定為記憶體中的向量的開始位置)
和索引(指定為向量中特定的元素的偏移量)

*LISP資料的表示
我們能使用向量來實現基本的數對結構,來滿足一個列表結構的記憶體的需求。
讓我們想象一下,計算機的內容被分成了兩個向量:the-cars 和 the-cdrs。
我們將表示列表結構如下:一個數對的指標是這兩個向量的一個索引。數對的頭
部是the-cars帶有特定的索引的入口,數對的尾部是the-cdrs帶有特定的索引的入口。
我們也需要其它的不是數對的物件的表示,(例如數字和符號)還有把一種與其它的
資料相區別的方式。為了完成這個任務,有許多的方法,但是它們都歸結為使用
型別的指標,也就是,為了在資料型別中包括資訊,對指標的概念進行擴充套件。資料型別讓系統
能夠區別出數對(由數對的資料型別與指向記憶體的向量的指標組成)的指標
與其它型別的資料(由其它的資料型別與能被用來表示那種型別的資料的東西)的指標。
如果兩個資料物件的指標是同一個的話,那麼我們認為這兩個物件是相同的。圖5.14顯示了
這個方法的使用來表示列表((1 2)  3  4),它的盒與指標圖也顯示了。我們使用字母字首來顯示
資料型別的資訊。因此,一個數對的指標帶有索引5被表示為p5,空的列表被表示為指標e0,
對數4的指標表示為n4.在一個盒與指標的圖中,我們在每個數對的左下邊,顯示向量索引,來指定數對的頭部和尾部被儲存在哪裡。在the-cars和 the-cdrs中的空白位置可能包括了其它的列表結構的部分。

((1 2)  3  4)   ------->[.][.]------->[.][.]------->[.][/]
                               1  |                2|                 4|
                                   |                  |                   |
                                  \/                \/                 \/
                                 [.][.]->[.][/]  [3]               [4]
                               5 |        7|
                                  |          |                  
                                  \/       \/    
                                 [1]      [2]

       索引     0    1      2     3     4      5     6     7   8    .....      
      頭部            p5    n3          n4   n1          n2        ....
      尾部            p2    p4          e0   p7          e0        .....
圖5.14   列表((1 2)  3  4)   的盒與指標圖和記憶體向量的表示法

對一個數的指標,例如n4,可能由一個顯示為數值資料的型別加上資料4的實際表示法組成的。
為了處理資料,為了一個單獨的指標,在固定廈的空間中,因為太大而不能被表示出來,我們能
使用一種大整數的型別,指標指定列表的資料的哪一個部分被儲存。

一個符號可能被作為一個型別的指標被表示,它是一個字母的序列,來形成符號的列印形式的表示。當字母的字串在輸入中被初始遇到時,這個序列被Lisp的讀組裝起來。因為我們要一個符號的兩個例項被識別出來,通過eq?得到“相同”的符號,我們要eq?為指標的相等性作最簡單的測試,我們必須能夠確保如果負責讀的程式在看到相同的字串兩次時,它將使用相同的指標來表示這些物件。為了完成這個任務,讀的程式維護了一個表格,傳統上被叫做obarray.當讀的程式遇到了一個字串時,將要組成一個符號,它檢查 Obarray,看一看它是否是與以前的字串相同。如果不同,它組成一個新的符號,並且把這個指標加入到obarray.如果與之前的相同,它返回儲存在obarray中的符號的指標。通過唯一的指標來代替字串的過程,被叫做內部化符號。

*實現原生的列表操作
給定如上的表示的模型,我們能夠把一個暫存器機器中的每個原生的操作代替成
一個或者是更多個向量的操作。我們將使用兩個暫存器,the-cars和the-cdrs,來標識
記憶體的向量,並且將假定vector-ref 和vector-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>))

通過分配一個未用的索引和在the-cars和the-cdrs中的索引向量的位置儲存cons的實際引數,
來執行了cons.  我們假定了一個特殊的暫存器,free,它總是儲存一個數對的指標,這個指標
包括了一個下一個可用的索引,我們能對那個指標的索引部分加一,來找到下一個可用的位置。例如,如下的指令:

(assign  <reg1>  (op cons)  (reg <reg2>)  (reg  <reg3>))

被實現為向量的操作中的如下的序列:
(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?
number?僅需要檢查型別域。

*實現棧
儘管我們的暫存器機器使用棧,我們在這裡不需要做格外的事,因為棧
能被用列表來模型化。棧能成為一個有儲存值的列表,指向一個特殊的
暫存器the-stack. 因此,(save  <reg>)能被實現為

(assign  the-stack  (op cons)  (reg <reg>)  (reg  the-stack))

相似的是,(restore  <reg>) 能被實現為
(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
畫出如下的程式生成的列表結構的盒與指標表示和記憶體向量表示(如圖5.14那樣)

(define  x  (cons  1 2))
(define   y  (list  x  x))

free指標初始化為p1,free的最後的值是什麼? 什麼指標表示x和y的值?

練習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!程式把兩個列表合在一起。設計一個
暫存器機器,來實現這些程式中的每一個。假定列表結構的記憶體操作是可用的,
是作為機器的原生的操作的。