GC與顯式內存管理

    C++復興的話題至今已被鼓吹兩年有餘,Herb Sutter和Bjarne Stroustrup等大牛們也爲C++帶來了大步伐的革新。然而,從這兩年的效果而言,C++的復興並無發生。一方面隨着世界經濟的動盪,IT行業也出現了必定程度的衰退;另外一方面這也是個新興語言如雨後春筍的時代,尤爲是web平臺上,CoffeeScript、Dart、TypeScript等,新人階前花更紅。拋開非技術緣由不談,我更有興趣的是,C++到底能佔據多大的性能優點,以實現其復興,尤爲是在內存管理上。

    Native復興論的主要論據一:不斷興起的移動設備性能有限並且電池續航需求高,且硬件難以再現過去20年的高速發展。事實。論據二:GC比顯式內存管理佔用更多的內存,且在內存不足時會出現性能問題;而C++11已經基本解決內存管理安全問題,因此能夠在不引入GC性能開銷的條件下實現GC的好處。(注:準確地說,引用計數邏輯上也算一種GC。)

    固然,C++是理論上能夠作任何極限的優化的,其極限性能必然超過使用GC的語言,因此這裏必須退一步,考慮通常狀況。由於若要復興,必然須要可以吸引佔多數的通常應用的開發者。在移動設備上,GC確實遠比桌面系統上的差,垃圾回收的開銷每每很大,能夠致使零點幾秒的阻塞,這對遊戲這樣有實時性需求的應用來講,是個大問題;對非實時但有UI交互操做的應用,也會影響界面響應的平滑度。爲了減小回收的開銷,又必然佔用更多的內存以便延遲迴收減小阻塞,而佔用更多的內存也可能致使更高的cache miss率。這樣,一個使用GC的語言,每每要使用3倍或更多的內存,而又面對內存並不豐富的移動設備。這看起來確實是C++能完勝的地方。

    而事實上,市場上並不乏使用Java、C#乃至JavaScript、Lua開發的移動設備實時應用。它們繞過GC性能問題的方法也和C++同樣,作顯式內存管理。用GC的語言作顯式內存管理聽起來有點怪,但其實多數也是C++裏經常使用的方法,好比啓動時預先分配對象內存,利用數組預留內存、實現複雜數據結構(當年寫過Basic程序的人應該沒少作這個)等,以便減小運行時動態內存分配。惟一作不了的就是C/C++的自定義內存分配器。事實上,在遊戲領域,自定義內存管理是很廣泛的事,C++的堆分配開銷相對實時需求每每仍是有些偏大,並且還有內存碎片問題,在後期優化階段,多數會被替換成預分配的大塊內存。

    所以,我傾向於認爲C++有優點但對通常應用而言並不是有絕對優點,C++的優點領域和之前相比並無太大的不一樣。對於GC的語言,在必要時,也是能夠作顯式內存管理的。

    附1、C++11的unque_ptr、shared_ptr性能討論:使用這些智能指針對象並不是沒有GC開銷。首先,對象的析構函數調用要引起成員智能指針的析構,對於大的對象結構,這至關於一次樹的遍歷。其次,unque_ptr、shared_ptr是線程安全的,這是一個很是好的特性的同時,也是須要必定的實現代價的。儘管它們是用遠比鎖高效的原子操做實現的,但原子操做仍然意味着不能緩存在寄存器,並且寫操做時會flush cache(數百時鐘週期的開銷),因此它們應被用來管理對象的ownership,而對不涉及ownership的參數傳遞等,直接用簡單的對象指針就好。

    相似的,Windows上COM對象指針的傳遞,按規則,全部的參數、返回值傳遞都要加減引用計數。這個儘管並不是使用原子操做、並不是線程安全,仍然致使不少冗餘的引用計數操做。因此D3D10開始使用了非標準的COM用法,以減小沒必要要的引用計數。

    附2、GC語言上連續內存分配的討論:說到預分配大塊連續內存,一般會最早想到struct array。這個.NET裏還有的用,但Java、JavaScript、Lua等就不支持了。用class也能作到預分配內存,但不是連續空間,cache miss率明顯大於struct。儘管如此,它們都支持primitive類型的連續內存數組,並且primitive的數組纔是性能最佳的數據結構。也就是說,內存不按對象分配而按屬性分配,使用position = new float[n * 3], velocity = new float[n],而不是class Bullet { float3 position; float velocity; } bullets = new Bullet[n]。這樣各個屬性值的內存佈局更加緊湊,因爲通常一個函數只會訪問對象的少數屬性,這樣緊湊的佈局會大幅提升cache的命中率。固然,也不是說非得用primitive不可,好比.NET用struct的話,可讓position變成struct float3的數組,更易讀易用一些。

    習慣於教科書式OO的人對這麼設計數據結構可能會感到不舒服,由於這彷佛破壞了OO。但我認爲,這只是實現細節,並不影響外部把它封裝成對象的集合。也能夠換個角度看,這只是另外一種OO的設計,只不過是以屬性集合做爲對象而已。相似方案也早就出如今Ogre 2.0草案裏,以縮小其和商用圖形引擎的性能差距。

    最後還要強調一下,這畢竟是在「fight the language」,並非個簡單的平常使用的設計,切莫過分使用。在性能可接受的條件下,可維護性優先。

    附3、GC的性能特徵:GC的性能特徵隨GC的類型不一樣而不一樣。現在主流可能是Mark and Copy類的,其特色是對生命期超長(好比從程序啓動到退出)的對象和生命期超短(好比僅限一個函數調用內部)的對象最高效,幾乎沒什麼開銷。儘可能避免finalizer,有finalizer的對象的回收代價很大,必需要用的,要用Dispose等顯式釋放。回收時堆掃描的性能和對象數量相關,就是說對中、長生命期的對象而言,少許大數組對象遠比大量小對象高效。 web

相關文章
相關標籤/搜索