1. 程式人生 > >5.3.2 維護無限記憶體的假象

5.3.2 維護無限記憶體的假象

5.3.2  維護無限記憶體的假象
在5.3.1部分中列出的表示方法解決了實現列表結構的問題,提供了我們有無限多的記憶體。
在一個真實的機器中,我們將在組裝新的數對的時候,最終耗盡了空閒的空間。然而,
在一個典型的計算中,生成的數對的大部分僅被用來儲存中間的計算結果。這些結果被讀取後,
數對就不再被需要了,它們是垃圾了。例如,如下的計算:

(accumulate  +  0  (filter  odd?  (enumerate-interval  0  n)))

組裝了兩個列表:累加值和過濾累加值的結果。當累加計算被完成後,這些列表就不再需要了,分配的記憶體能被重新申請。如果我們能安排週期性地回收這些垃圾的話,在我們組裝新的數對時,如果能以相同的速度重用記憶體的話,我們將能保持著這裡有無限的記憶體的假象。

為了回收數對,我們必須有一個方式來確定哪些被分配的數對已經不再需要了(即這些內容不
再影響計算的未來的結果)為了完成這個任務,我們將使用的方法是知名的方法,叫做垃圾回收。垃圾回收是基於如下的事實,在一個lisp的解釋的任何的時候,能影響計算的未來的結果的僅有
的物件是那些通過一系列連續的取頭部或者是取尾部的操作,能到達的物件,在暫存器機器中,從指標的當前的位置開始進行操作的。任何記憶體的格子不再被讀取可能被回收。

有許多的方式實現垃圾回收。在此我們檢查的方法被叫做停止與複製。基本的思想是把記憶體分成
兩個部分:工作記憶體和空閒記憶體。當cons組裝數對時,它分配它們在工作記憶體。當工作記憶體滿了,我們通過把在工作記憶體中的所有的有用的數對分配,並且複製到空閒記憶體的連續的位置上,來執行垃圾回收。(通過跟蹤所有的取頭部和取尾部的指標,有用的數對能被定位,以暫存器機器開始)。因為我們沒有複製垃圾,這就成為了新的空閒的記憶體空間,我們能用來分配新的數對了。此外,在工作記憶體中沒有被需要的東西了,因為所有的有用的數對都被複制了。因此我們交換了工作記憶體與空閒記憶體的角色,我們能夠繼續處理:新的數對將被分配在新的工作記憶體(它是舊的空閒記憶體),當它滿了,我們能複製有用的數對到新的空閒記憶體中(它是舊的工作記憶體)。

* 一個停止與複製的垃圾回收器的實現
我們現在使用暫存器機器的語言以更詳細的程度來描述停止與複製演算法。
我們將假定有一個暫存器叫做root,它包括了一個指向最終指向所有的可讀取的資料的資料結構的指標。這能被安排,通過儲存所有的機器的暫存器的內容在一個預分配的列表,這個列表指向root.root僅位於開始垃圾回收之前。我們也假定,除了當前的工作記憶體,有空閒的記憶體可用於我們能夠複製的有用的資料。當前的工作記憶體由向量組成,向量的基地址在暫存器中,這暫存器叫做the-cars和the-cdrs。空閒的記憶體在暫存器中被叫做new-cars和new-cdrs.

在當前的工作記憶體中,當我們耗盡了空閒的記憶體格子時,垃圾回收被觸發,也就是,
當一個cons操作試圖增加空閒指標時,發現它將要指向記憶體的向量的結尾之外了。
當垃圾回收的過程被完成,root指標將指向新的記憶體,從root可讀取的物件都被移動到了
新的記憶體中,空閒指標將顯示著在新的記憶體中,當一個新的數對能被分配的位置。此外,
工作記憶體與空閒記憶體的角色將被互換,新的數對將被組裝在新的記憶體中,開始於空閒指標的位置,之前的工作記憶體將作為新的記憶體可用於下一次垃圾回收。圖5.15顯示了記憶體的安排,在垃圾回收之前的樣子和之後的樣子。

                    垃圾回收前            
               ——————————————
the-cars  |     混合了有用的資料與垃圾        |      工作記憶體
the-cdrs  |                                                 |
               |_________________________________|
                                                               ^
                                                                |
                                                               空閒指標                            

                ——————————————
new-cars  |     空閒記憶體                               |      空閒記憶體
new-cdrs  |                                                 |
                |_________________________________|

                    垃圾回收後            
                ——————————————
new-cars  |     放棄的記憶體                            |      新的空閒記憶體
new-cdrs  |                                                 |
                |_________________________________|
                         

               ——————————————
the-cars  |    有用的資料 |  空閒的區域         |      新的工作記憶體
the-cdrs  |                     |                            |
               |______________|___________________|
                                      ^
                                       |
                                 空閒指標  
圖5.15 通過垃圾回收過程的記憶體的重配置

通過維護著free和scan這兩個指標,來控制著垃圾回收過程的狀態。

它們被初始化指向新的記憶體的開始處。演算法開始於把數對的指向由root,重定位到
新的記憶體的開始處。這個數對是被複制的,root指標被修改為指向新的位置,並且free
指標被增加。此外,數對的舊位置也被標記,顯示著它的內容被移動了。這個標記過程被做如下:在car位置,我們設定一個特殊的標識,表明這個是一個已經移動了的物件。(這樣的物件在傳統上的叫做是一個損壞的心)在cdr位置,我們設定一個前向的地址,它指向已經被移動了的物件的位置。

重定義了root後,垃圾回收器進入了它的基本的迴圈之中。在演算法中的每一個步驟,
scan指標都指向已經移動到新的記憶體的數對,但是它的取頭部和取尾部的指標仍然指向
舊記憶體中的物件。這些物件每有一個被重定位,scan指標就被增加。為了重定位一個物件,我們檢檢視看是否物件已經被移動了。如果物件沒有被移動,我們複製它到空閒指標顯示的位置,再更新空閒指標,並在物件的舊位置上設定一個失效的標誌,再更新物件的指標指向新位置。
如果物件已經被移動了,它的前向地址被設定為被掃描的數對的指標。最終的,所有的可讀取
的物件將被移動和掃描過,scan指標將超過空閒指標,過程就終止了。

我們能把停止與複製的演算法寫成一個暫存器機器的指令的序列。以一個叫做relocate-old-result-in-new的子程式來完成重定義一個物件的基本步驟。這個子程式得到了它的實際引數,被重定位的物件的指標,它的暫存器名稱是old。它重定位目標物件,把重定位物件的指標給一個叫做new
的暫存器,並且返回儲存在暫存器relocate-continue中的入口點。為了開始垃圾回收,我們能呼叫這個子程式來重定位root指標,在初始化了free和scan之後。當root的重定位已經完成的時候,我們安裝新的指標作為新的root,並且進入垃圾回收器的主迴圈中。

begin-garbage-collection
  (assign free (const 0))
  (assign scan (const 0))
  (assign old (reg root))
  (assign relocate-continue (label reassign-root))
  (goto (label relocate-old-result-in-new))
reassign-root
  (assign root (reg new))
  (goto (label gc-loop))

在垃圾回收器的主迴圈中,我們必須確定是否還有更多的物件被掃描。我們做這個事是通過
測試scan指標是否跟上了free指標。如果指標相等,那麼所有的可讀取物件都已經被重定位了,
然後我們到gc-flip分支上去,它負責後面的事,為了我們能繼續中斷的計算。如果還有要掃描的
數對存在,我們呼叫重定位的子程式,來重定位下一個數對的頭部(通過設定在舊記憶體中的頭部指標)。relocate-continue 暫存器被設定為了子程式返回後來更新頭部指標。

gc-loop
  (test (op =) (reg scan) (reg free))
  (branch (label gc-flip))
  (assign old (op vector-ref) (reg new-cars) (reg scan))
  (assign relocate-continue (label update-car))
  (goto (label relocate-old-result-in-new))

在update-car程式中,我們修改了被掃描的數對的頭部指標,然後接下來重定位數對的尾部。
當重定位已經完成後,我們返回到update-cdr。重定位和更新的尾部後,我們完成了對那個
數對的掃描,所以我們繼續主迴圈。

update-car
  (perform
   (op vector-set!) (reg new-cars) (reg scan) (reg new))
  (assign old (op vector-ref) (reg new-cdrs) (reg scan))
  (assign relocate-continue (label update-cdr))
  (goto (label relocate-old-result-in-new))

update-cdr
  (perform
   (op vector-set!) (reg new-cdrs) (reg scan) (reg new))
  (assign scan (op +) (reg scan) (const 1))
  (goto (label gc-loop))

relocate-old-result-in-new子程式重定位物件如下:如果被重定位的物件不是一個數對,
那麼我們返回沒有被改變的物件的相同的指標。(例如,我們可能掃描一個數對,它的頭部是4
如果我們表示它的頭部為n4,正如在5.3.1部分中掃描的那樣,然後我們要重定位的頭部指標仍然是n4)。否則,我們必須執行重定位。如果數對的頭部的位置被重定位包括了廢棄的標誌,那麼
數對在事實上已經被移動了,所以我們檢查前向地址(從廢棄的標誌的尾部)並且返回這個在新的記憶體的指標。如果指標在舊的記憶體中指向一個未移動的數對,那麼我們移動這個數對到新的記憶體的第一個空閒的記憶體位置,並且在舊的位置上設定廢棄的標誌和前向地址。relocate-old-result-in-new子程式使用一個暫存器oldcr來儲存舊的記憶體中的物件的頭部和尾部。

relocate-old-result-in-new
  (test (op pointer-to-pair?) (reg old))
  (branch (label pair))
  (assign new (reg old))
  (goto (reg relocate-continue))
pair
  (assign oldcr (op vector-ref) (reg the-cars) (reg old))
  (test (op broken-heart?) (reg oldcr))
  (branch (label already-moved))
 (assign new (reg free)) ; new location for pair
  ;; Update free pointer.
  (assign free (op +) (reg free) (const 1))
  ;; Copy the car and cdr to new memory.
  (perform (op vector-set!)
           (reg new-cars) (reg new) (reg oldcr))
  (assign oldcr (op vector-ref) (reg the-cdrs) (reg old))
  (perform (op vector-set!)
           (reg new-cdrs) (reg new) (reg oldcr))
  ;; Construct the broken heart.
  (perform (op vector-set!)
           (reg the-cars) (reg old) (const broken-heart))
  (perform
   (op vector-set!) (reg the-cdrs) (reg old) (reg new))
  (goto (reg relocate-continue))
already-moved
  (assign new (op vector-ref) (reg the-cdrs) (reg old))
  (goto (reg relocate-continue))

在垃圾回收的過程的末尾處,我們交換了新舊記憶體的角色,通過交換指標的方式:
把the-cars和 new-cars進行交換,把  the-cdrs 和new-cdrs進行交換。
我們將準備在下一次記憶體耗盡時,執行另一次垃圾回收。

gc-flip
  (assign temp (reg the-cdrs))
  (assign the-cdrs (reg new-cdrs))
  (assign new-cdrs (reg temp))
  (assign temp (reg the-cars))
  (assign the-cars (reg new-cars))
  (assign new-cars (reg temp))