評價 GC 演算法好壞的幾個標準
大學時候讀《松本行弘的程式世界》的時候,粗略的瞭解了一下垃圾回收演算法,寫過一篇ofollow,noindex" target="_blank">介紹三種最基本的垃圾回收演算法的文章 。最近在看《垃圾回收的演算法與實現》,接觸到了這些垃圾回收演算法的實現細節。粗略瞭解的話,基本的原理就是之前這篇部落格介紹的內容,這樣說出來很簡單,也容易理解。但是實現起來就有很多細節了,首先要用到很多指標,所有的物件和引用的基礎都是指標。然後會用到連結串列來儲存一些可用記憶體,用堆來分配物件等。
其實在學校的時候學了很多資料結構,工作了發現一隻在 CURD,但是研究 GC 這個話題會用上那些複雜的資料結構和演算法,優化的空間也很大,非常有意思。
從這些演算法細節也可以看出這些不同的演算法其實都有一些強大的地方和弱點。好在評價一個垃圾回收演算法的好壞有比較明確的標準,這篇文章就參考《垃圾回收的演算法與實現》這本書談一下評價標準。
吞吐量
垃圾回收演算法(6 個字太長了,以下簡稱 GC)算是對程式完成它想做的事情的一種輔助,並不是程式的主要目的(廢話)。所以 GC 佔用的時間越少越好,程式花在正事上面的時間越多越好。
這個標準嚴格定義應該是 需要處理的堆大小 ÷ GC 佔用時間。書中稱為“吞吐量”。
這個指標其實很好理解。舉個例子吧,標記清除演算法要遍歷兩次,第一次遍歷所有活躍的物件,將它們標記為“不是垃圾”。第二次遍歷所有的物件,將沒有被標記的垃圾回收掉。複製收集演算法只需要遍歷一次,將活躍的物件從記憶體的一般複製到另一半。所以從這個指標的角度講,複製收集演算法完爆標記清除。
但實際上,這個指標是不能“靜態衡量”的。依然是上面兩種演算法,標記清除遍歷的時候速度很快,只要寫一個標記就可以了。而複製收集演算法要有 copy 操作。雖然堆中活動物件的增加,甚至會出現複製收集吞吐量小於標記清除的情況。
記憶體使用率
這個指標也比較好理解,GC 演算法需要一些標記,但是如果演算法本身所使用的記憶體佔得很多,就得不償失了。這個演算法本身的目的就是回收記憶體,本身卻佔用了很多記憶體,聽起來就不合理。
其實,從演算法的角度講,記憶體是空間,吞吐量是時間。演算法上有“用空間換時間”的策略,自然 GC 也會有。這是一種 Trade off,很多情況不可兼得。
拿引用計數來說,用幾個位來表示引用計數是門學問。簡單的話,佔用 8 個位表示,那每個物件 1byte 就沒有了。假設是佔用 2 個位元組的物件,那麼記憶體佔用就擴大了整整 1.5 倍。 而大多數物件僅僅會被引用 1 次而已。所以引用計數方法就發展出一些優化措施,減少引用計數佔用的記憶體,配合其他演算法來處理計數器溢位的問題。有一種極端的方式是隻使用 1 位來計數(倒不如說這是一種標記了)。叫做 1位引用計數。具體的原理,這篇文章就不展開說了。
複製清除演算法每次只能使用一半的空間,所以這個演算法的記憶體使用率也是很低的。
最大暫停時間
這個指標和“吞吐量”看起來有些像,GC 演算法速度越快,時間就越小。但是吞吐量指的是總體的速度,最大暫停時間指的是 GC 演算法執行的時候,程式在等待 GC 完成的最大時間。這段時間由於 GC 的執行程式無法做其他的事情。
為什麼這個指標會重要呢?有些程式可能可以忍受吞吐量不高,但是實時性要求很強的。比如說,A 演算法每分鐘暫停 1 次,一次暫停 1 秒,一小時一共暫停 60s。另一個 B 演算法每小時暫停 1 次,一次暫停 30s。可想而知,如果是使用者程式暫停時間長是體驗很糟糕的,另外比如機器人控制程式,邁開一隻腿這時候到了暫停時間,機器人就摔倒了。
引用計數的最大暫停時間是最好的,因為物件的引用到 0 的時候立即會回收,這個過程不需要暫停程式。而複製演算法和標記清除演算法需要在無法申請出更多記憶體的時候,暫停程式,開始清除階段/複製階段。
連續性(快取友好程度)
我們知道,越快的儲存價格就越高。CPU 暫存器速度最快,但是隻有幾個暫存器。快取記憶體非常快,但是極其昂貴。然後是記憶體、硬碟等。
如果程式在執行的時候快取命中率高,那麼執行效率就會高。
在這篇文章中,我們可以將程式粗略的分成兩部分:GC 執行的部分和程式執行的部分。如果 GC 執行的時候需要頻繁尋找物件,然後物件的引用又在很遠的地方,那麼快取命中率就會很低;另一方面說,如果 GC 演算法將程式的物件變得很離散,那麼程式在執行的時候,互相引用的物件離得很遠,效率就會很低。
標記清除演算法會造成記憶體的碎片,對快取不友好。複製收集演算法由於是拷貝不是垃圾的物件,所以在一次拷貝操作之後,垃圾物件被釋放,非垃圾物件都在一起了,所以命中率會高。另外上面提到的 1 位引用計數演算法由於只拷貝指標,而不需要去找到物件,所以快取命令率也會高。
是否寫入操作(copy-on-write 友好程度)
Copy-on-write 在現代計算機技術中是比較常用的。簡單來說,如果你 fork 出 4 個程序,而這 4 個程序都只是在讀同一片記憶體,系統將會將這一片記憶體共享給 4 個程序,如果有其中一個程序想要寫入的時候,系統才會複製出來這部分記憶體給這個程序寫入。
這裡特別要“批評”的是引用計數,因為引用計數的原理是每一個物件記住有多少個物件指向了自己。這就意味著,即使我們沒有更改這個物件,只是讀(應用)了這個物件,相關的記憶體就有了寫入操作,這部分記憶體就會被複制。
2017 年 Instagram 發表的這篇關閉 Python 的 GC 來提高效能 的文章,原理就是這樣。
以上內容現學現賣,如有錯誤歡迎指出。