為什么我支持托管運行時(虛擬機)

作者: 趙劼  發布時間: 2010-08-08 20:46  閱讀: 1253 次  推薦: 1   原文鏈接   [收藏]  

  最近博客園上在炒關于C#性能的問題,其實應該說是.NET性能的問題,其中某位仁兄提出,他希望C#能夠直接編譯為原生代碼,而不是在CLR這樣一個托管運行時上執行,因為虛擬機啊,JIT什么的性能差。后來發到TL上以后,也有朋友認為,“基于虛擬機的語言都是大公司為了利益在推動,說白了就是政治”,因此“對C#提高性能的建議感到可笑,因為它本來就不是用來開發高性能程序的”,再有,“C、C++已經明確不和這些后進爭所謂的‘容易開發’的頭銜”,那么其他語言為什么要和C++它們比較性能呢?我是托管運行時,或者虛擬機的忠實擁護者,這里談一下我在這方面的看法。

  我并不反對編譯為原生代碼的語言,尤其是C語言,它的意義在于提供了一種對硬件完全控制的手段,對硬件提供了一種最直接的抽象,幾乎可以映射到最終流程控制方式,因此無可替代。C++作為C語言的超集,提供了更豐富的抽象能力(如面向對象和模版化),只是語言本身過于復雜,超過了以我的智商可以承受的范圍,因此我學了幾次都沒怎么學會,現在更是忘得差不多了。不過我認為,越來越多的語言會構建在托管平臺上,而不是直接編譯成原生代碼。因為一個統一的托管運行時會帶來很多好處。

  首先,統一運行時提供了跨平臺的能力,Java便是一典型。.NET上有mono,使用也很廣泛,也有不少Unity3D,Gnome DO等成功案例。Novell,包括其他一些公司也在銷售基于mono的商業產品(如MonoTouch及Infragistics的ASP.NET Controls組件),我本身也在兩年多前就在生產環境上使用了mono,您現在看到的這個博客也是基于Ubuntu Server、mono 2.6 、Apache以及微軟開源的ASP.NET MVC 2構建的。雖說從某些層面(如API兼容性)上說,mono的跨平臺性遠不如Java平臺,但它也是一個比較成熟的執行環境,并具備相當程度的跨平臺能力——尤其是在mono上開發,MS .NET上運行的時候(雖然我不建議這么做)。如今支持mono的產品、類庫數不勝數,我時常調侃道,“如果您的產品不支持mono,還真不好意思和人打招呼”。只可惜,不少人都用一些“不是親娘生的”類似的調調來否定mono,在我看來沒有經過調查研究的看法只能屬于“臆斷”,而且更是一種FUD了。

  即便退一步來說,我們不“跨操作系統”吧。有人說,.NET就支持Windows么,何必搞個虛擬機,還JIT那么麻煩。但事實上,“跨平臺”并非指的是簡單的“跨操作系統”,而是“跨執行環境”,如Silverlight。事實上,跨計算機體系結構本身也是種跨平臺(當然,操作系統其實已經進行了一定的統一抽象了)。因此,虛擬機的目的,是為上層執行體抽象出了統一的運行環境——這其實還是跨平臺,這平臺不僅僅是指操作系統,整體運行環境之間的差異也是運行時所“抽象”的一部分。比如在并發環境中,不同CPU架構的流水線上的亂序方式不一樣,同樣的代碼執行的效果就可能不同。最典型的例子,便是JVM之前的內存一致性模型控制的比較寬松,導致經典的double check模式在某些CPU上是會失敗的。現在Java標準也變得嚴格了,和.NET CLR一樣避免了Store Reordering。這意味著在某些CPU上,會在特定的地方加上Memory Barrier保證執行效果的一致性。在我看來這是更好的可移植性。C++或是C語言等實現“可移植性”的方式,往往是通過為不同環境提供不同的編譯器,生成不同的結果,而且會使用“宏”等方式,在代碼里寫出有平臺針對性的代碼。

  有了統一的運行時,也可以讓多語言互操作更為容易,如果沒有JVM或CLR,就很難像現在這么輕松地在Scala/Java/Jython/JRuby,或是C#/F#/IronPython/IronRuby,甚至是未來的語言之間進行直接地互操作,更難做到“無縫集成”了。在合適的場景下選用合適的語言,是提高生產力的重要手段。如果沒有JVM平臺,就很難使用Scala來代替Java這樣的劣質語言,而現在Scala便能夠保證充分利用Java平臺上類庫等積淀。

  有了“多語言”,那么便會引出虛擬機的另一個作用:讓語言實現者和虛擬機實現者的工作可以分離開來,各自優化。虛擬機的實現者可以盡力優化虛擬機的各種表現,為虛擬機加入各種優化措施,而無需讓虛擬機的上層語言分別調整。例如JVM性能提高之后,Scala、Java、Jython、JRuby等語言的性能都可以提高,否則各種語言分別優化的代價太高了(當然某些情況還是需要特殊對待的)。要說這方面的實例,在.NET平臺上有個開源的組件DLR(動態語言運行時),是在CLR上提供編寫動態語言的統一輔助類庫。以前它有個缺點,便是啟動速度比較慢,因為動態代碼的JIT耗時較長,而動態語言的很多場景是“即用即拋”的。后來,DLR增加了一個優化策略,便是一開始直接對抽象語法樹直接解釋執行,而執行多次之后才使用后臺線程將代碼JIT成原生代碼。有了如此改進,基于DLR的動態語言,如IronRuby和IronPython的啟動速度都提高了。

  虛擬機/運行時本身可以做的優化也很多,如果真覺得性能不夠,那么完全可以在運行前在本地編譯成native code,這和直接從源代碼變成native code從結果上看沒什么區別。但是現實是,很少有人去這么做,因為這么做往往只是節省了JIT的開銷,對性能提高效果不大。在不同環境中,此外還有各種優化,比如使用解釋執行,而不是JIT以次節省內存消耗,或是在運行時回收JIT的代碼(印象中在.NET Compact Framework里有這樣的策略,求詳情),或是在運行時根據代碼邏輯進行二次編譯。下面會談一個例子。另一種典型的優化,一直在研究卻還沒有真正實現的,虛擬機便是“自動并行”。關于這點,Anders在上次的演講中多次提到過,要實現這點還需要有各種支持,如聲明式編程,提供“無副作用”標記,甚至在語言級別的支持等等。

  之前那位朋友提到,C/C++已經明確不與后進語言比生產力了,后進語言也沒辦法和它們比較性能。對這個觀點我持保留意見,因為基于虛擬機的做法,其優化空間還有很多,理論上也可以做到更為徹底,在許多情況下性能完全有超越靜態編譯語言的可能。這是許多人的看法,而事實也是如此,在某些場景下Java的性能也已經超過了C++。如回到上面的二次編譯優化,對于性能優化也大有好處。舉一個簡單的例子,面向對象語言會出現很多虛方法調用(尤其是在符合一些關于設計的最佳實踐時,如“基于抽象編程”),調用需方法需要查方法表找方法入口,最普通的做法就是必須每次根據對象的實際類型查找方法表,找到地址,然后調用。偽代碼如下:

 
根據實際類型找到函數入口
調用

  但是在實際執行過程中,可能有某個特定類型出現的次數特別多,甚至完全只會出現一種具體類型的實例(抽象只是為了擴展性或是單元測試等等),但因為虛方法的調用語義,我們就無法對這次調用進行內聯優化等等。不過在托管的運行時中,如果發現某個特性類型出現次數特別多,則還是可能將那個類型的方法內聯進來,然后只在“不是那個實例”的情況下才去查找方法入口。偽代碼就是:

 
if (對象為不是類型A) then
根據實際類型找到函數入口
調用

else
執行內聯后的類型A的代碼
end

  類似這種的優化不是空中樓閣,它們在HotSpot(即SunOracle的JVM)是確切實現的。Hotspot的JIT,尤其加上-server開關時,它的優化會變得十分激進,而CLR的JIT與它相比就像是靜態編譯器了。C++在科學運算中性能高,往往是靠大量的inline和精細資源控制,但是這說實話都還是靜態的“手動優化”,如果比實際工程的真實運行情況,比如上面說的虛方法跳轉,還真不見得C++可以超過托管代碼。虛擬機做的則是動態優化,對于長期執行的代碼甚至可能執行的越來越高效,還能夠針對環境改變作出調整。總而言之,托管代碼可以運用的優化策略實在太多了。隨著技術發展,托管代碼的速度可以進一步提高,而靜態編譯代碼的空間就小得多了。

  對此Milo Yip老大做過一些補充,他指出這方面不同的C++ compiler有不同優化方法,例如VC2008開始有的Profile-Guided Optimizations:

Virtual Call Speculation - If a virtual call, or other call through a function pointer, frequently targets a certain function, a profile-guided optimization can insert a conditionally-executed direct call to the frequently-targeted function, and the direct call can be inlined.

  我簡單地思考了一下,從理論上說,各種優化策略都可以通過靜態編譯的方式寫入原生代碼,因此JIT能做的事情,native code理論上都能做。不過,這就意味著要在每個程序中帶上“負責優化”的代碼,而不光是程序的“功能代碼”。用虛擬機,就意味著所有的程序都共享了虛擬機的這么一套優化機制。如果虛擬機的優化手段改進了,那么所有基于虛擬機的程序都能獲利,而靜態編譯的程序,往往只能靠“重新編譯”來得到優化了。

  此外,對于面向對象語言來說,虛函數是很常見的,虛函數之間各種組合調用,分支、路徑也特別多,到處都可能使用虛函數。我“猜想”,像VC這樣的編譯器,為每個虛函數調用之處都提供了基于profiling的內聯優化是不太現實的。而且,為每個虛函數調用的地方都提供“內聯”和“不內聯”兩種版本也不太可能。而基于虛擬機的話,它就可以動態的進行各種優化,每段JIT后的代碼都可以回收再動態生成,這種優化是靜態編譯難以比擬的。

  誠然,就目前來說,就性能方面,許多情況下還是靜態編譯配合手動細節優化可能獲得更好的效率,但利用托管運行時獲得的好處也是非常可觀的,而且我也一直沒有遇到過這方面的效率問題。同時,我認為托管運行時的愿景也十分現實可靠,因此就我看來,托管代碼是未來趨勢,除了越來越小的特定領域,越來越多的程序和語言會構建于托管平臺上。事實上,正因為上面這些好處(例如獨立優化),越來越多的語言也開始加入虛擬機這樣的策略了。例如Rubinius,PyPy等新的Ruby和Python語言實現,其實都是有個虛擬機這樣的機制存在。

  最后我再多說一句:我并不是說追求性能的做法不對,我也從來不會說追求性能本身沒有意義。但是我不同意說“考察托管語言性能”沒有意義,更是完全不同意說托管代碼虛擬機“本身意義就不大”,“完全是大公司在推動”,或是“政治因素”云云。技術就是技術,這些技術上投入了無數天才的精力和創意,這不是什么“政治”之類說法就能帶過的

1
0
 
 
 

文章列表

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()