看網上好多Java程序員都說由於JIT技術的引入,Java的性能已經和C++一樣了,而且Java的開發效率極高,可以省下60%的時間。請問事實真的是這樣嗎?我平常也都在寫這兩個語言,但是因爲開發的軟件的複雜度不大,並沒有感覺到性能和開發效率有太大的差異,如果真的如那些Java程序員所說的那樣,爲什麼主流的遊戲引擎都不用Java實現呢?而且教育版的Minecraft爲什麼要用C++重寫呢?

不加限定語就說“Java性能已經達到甚至超過C++”純屬耍流氓 >_< 這種對Java性能的過分自信,作爲參與過HotSpot VM和Zing VM的實現的俺來說也無法認同。

要是有人跑了benchmark然後說Java的性能比C++好,俺的第一反應也會是:真的麼?得看看這benchmark到底測的是什麼,有沒有錯誤解讀結果。

反之亦然。不加限定語就說C++的性能完勝Java同樣屬於耍流氓,不過俺遇到這種論調通常都不會試圖去爭辯,因爲對方多半不是有趣的目標聽衆…

說到底,C++在比Java更底層的位置上,擁有更靈活的操縱接近底層的資源的能力,所以給充分時間做優化的話,無論如何也能比Java寫的程序跑得快。所以一旦討論向着這種不考慮開發時間的方向發展之後,通常就沒啥可討論的了,C++必勝。

有趣的限定語之一就是應用場景。例如說磁盤或網絡I/O爲主的應用類型,如果用Java實現,並且如果正確使用了Java中此場景下zero-copy的技巧,跟一個同算法用C++實現的版本性能不會有多少差別的。

這就是爲什麼單純用C++來重寫一次Hadoop(而不改進其設計或算法)的話並不會有顯著的性能提升——這樣的重寫並不會成就一個跟原裝MapReduce相同水平的系統。

有趣的限定語之二是microbenchmark。Benchmark可以用來測許多不同的方面,例如應用的啓動速度、頂峯速度等。然而一個使用JIT編譯或者自適應動態編譯的JVM,其性能必然是慢慢提升的;而一個AOT編譯的系統(例如C或C++,或者比較少見的JVM),它在代碼執行的層面上一開始就已經在最終狀態了,啓動時就以接近頂峯速度來運行。在這種前提下用寫C++的microbenchmark的方式去測Java的頂峯性能,那就是純打壓Java一側的分數大殺器。

如果要在常見JVM上測Java的頂峯性能,通常需要正確的預熱。使用專門用來寫microbenchmark的框架,例如jmh,可以有效地在進入測分階段前正確預熱。否則的話請選用一個做AOT編譯的JVM。

題主的原問題之一的:

而且教育版的Minecraft爲什麼要用C++重寫呢?

我不知道教育版Minecraft是神馬狀況,但原版Minecraft可是寫得很爛的Java。硬要說的話是Java代碼的反面教材(即便它那麼流行)。這個就算不用別的語言重寫,它的Java版本也早該重寫了(1s

==============================================

放個半相關的傳送門吧:LLVM相比於JVM,有哪些技術優勢? - RednaxelaFX 的回答

像C或者C++,在事先編譯(AOT)時可以充分利用closed-world assumption來做優化,而且用戶對編譯時間的容忍程度通常比較高(特別是對最終的product build的編譯時間容忍度更高),所以可以做很多相當耗時的優化,特別是耗時的interprocedural analysis。

我所熟悉的JVM中,JIT編譯最缺失的優化就是interprocedural analysis。JVM很可能會選擇用非常受限的形式的interprocedural analysis來做優化,例如CHA(Class Hierarchy Analisys);又或者是藉助大量的方法內聯(method inlining)來達到部分interprocedural analysis的效果。

即便是上面的傳送門舉的例子,Zing JVM裏基於LLVM寫的新JIT編譯器,還是隻做非常有限的interprocedural analysis的。例如說如果我們可以有whole-program alias analysis的話就無敵了,但是沒有…

(有些特化的JVM會在AOT編譯時做有趣的interprocedural analysis,例如Oracle Labs的Substrate VM。論文之一可參考 Safe and efficient hybrid memory management for Java )

==============================================

不過反過來,有些C++程序員覺得新奇(其實C++編譯器也已經做了很久或者漸漸開始流行起來)的優化,在高性能JVM上倒是稀鬆平常的事。

一個是PGO,profile-guided optimization。常見的JVM會不斷收集運行中的程序的type profile、branch profile、invocation/loop profile等,並將其應用到JIT優化中。再放個半相關的傳送門:JIT編譯,動態編譯與自適應動態編譯 - 編程語言與高級語言虛擬機雜談(仮) - 知乎專欄一個是LTO,link-time optimization。在常見JVM上運行的Java程序,本來就是做lazy/dynamic linking的,而等到JIT編譯的時候正好dynamic linking已經做好了,所以可以完全利用上linking後的狀態來做優化。這樣,跨模塊的優化(例如說跨模塊的方法內聯)就是稀鬆平常的事。看看常見的C++環境中,跨越動態鏈接庫的邊界會不會做函數內聯?這事情不是完全不能做,但是要做就得把一個優化器帶在運行時庫裏動態去做,對很多C++程序員來說這是不可思議的(再次強調,這不是不能做,只是很多人不習慣這種做法)一個是虛函數內聯,inlining of virtual method invocations。很多C++程序員會說C++的虛函數通過多態指針去調用的話無法內聯,一說起虛函數調用就想到vtable。這個當然也是誤解,其實C++編譯器可以實現若干技巧來實現虛函數的內聯,並非一定要通過vtable去調用。但在高性能JVM裏如果不能高效地內聯虛方法的話,性能就徹底完蛋了,所以不得不使用各種技巧來內聯。再次放個半相關的傳送門:HotSpot VM有沒有對invokeinterface指令的方法表搜索進行優化? - RednaxelaFX 的回答送上介紹C++編譯器通過CHA來做虛函數的去虛化(devirtualization)的論文:Optimization of Object-Oriented Programs Using Static Class Hierarchy Analysis, Jeffrey Dean, David Grove, Craig Chambers, ECOOP'95

對於我們做編譯器優化的人來說,Java相比C++最爽(適合優化)的一點就是指針的類型安全:Java裏,一個引用(底下由直接指針實現)聲明是什麼類型的,就可以相信它一定是什麼類型的;而在C++的優化編譯器裏,指針類型通常被認爲是不可信的,只有在非常高優化級別用盡一切可壓榨的信息來優化時纔會相信它。

這意味着,在Java裏,下面的方法:

static int foo(int[] a, byte[] b) { // ...}

可以直接通過類型信息就靠譜地得到a與b不會alias的結論,因爲a與b的靜態類型不兼容,運行時肯定不會指向同一對象。而相似的C或C++代碼:

int foo(int* a, char* b) { // ...}

則不能單純通過類型信息而得到a與b一定不alias的結論。

此外,Java的引用只可能引用對象的固定位置(例如說對象的起始位置),而不像C或C++的指針可以指向到對象的任意位置(例如任意指向到對象的內部)。所以通過一個Java引用去訪問不同的offset,也足以確定這兩個offset是不會alias的。

這就是說,在Java裏,下面的方法:

static int foo(int[] a, int[] b) { a[0] = b[1]; // ...}

雖然a和b的靜態類型都是int[],無法通過類型信息來判斷a與b不alias,但接下來訪問a[0]與b[1]肯定不會alias。

而相似的C或C++代碼:

int foo(int* a, int* b) { a[0] = b[1]; // ...}

不但a與b是否alias無法確定,a[0]與b[1]是否alias也無法確定。

至於alias analysis對優化有多重要,相信同行們會會心一笑。在HotSpot VM的Server Compiler(C2)裏,基於type + offset結合memory slicing做的alias analysis還是頗爲有效的。

==============================================

關於GC嗯…放幾個傳送門:

爲什麼 Python 工程師很少像 Java 工程師那樣討論垃圾回收? - RednaxelaFX 的回答Go1.6中的gc pause已經完全超越JVM了嗎? - RednaxelaFX 的回答Java 大內存應用(10G 以上)會不會出現嚴重的停頓? - RednaxelaFX 的回答Azul Systems 是傢什麼樣的公司? - RednaxelaFX 的回答C++ 短期內在華爾街的買方和賣方還是唯一選擇嗎? - RednaxelaFX 的回答

==============================================

有些關於Java性能比C++好的幾種常見誤解點,這裏也想稍微討論一下。

0、用Java程序的性能跟GCC / Clang的-O0、-O1來比性能。

答:這不是自取其辱麼。像HotSpot VM、IBM J9 VM裏的JIT編譯器,在其頂層編譯的時候,默認自帶的優化程度至少是跟GCC -O2在同一水平的——或者說那是目標。如果通過抑制對比的對方的優化程度來做比較,那有啥意思了。

1、JVM通過JIT編譯可以更好的利用CPU的特定指令,比C++事先編譯(AOT)的模型好。

答:並不是。看情況。有很多情況會影響這種說法在現實中的有效性。

其一是某個具體的JIT編譯器到底有沒有針對某些CPU的新指令做優化。如果沒有,那說啥都是白說。而像GCC、LLVM這些主流編譯器架構背後都有很多大廠支持,對新指令的跟進是非常快的——至少比HotSpot VM的JIT編譯器對x86的新指令的跟進要快和全面。如果本機用的程序自己在本機上-march=native來編譯,那便是對自己機器的CPU特性的最好利用。

而像Intel的icc所帶的庫,很多都是對各種不同的Intel CPU事先編譯出了不同的版本,一股腦帶在發佈包裏,到程序啓動時檢測CPU特性來選擇對應最匹配的版本的庫動態鏈接上,這樣也可以充分利用CPU的特性。

總之不要以爲用了JIT編譯器就比AOT編譯器能更充分地使用CPU特性了…這沒有必然的因果關係。

2、JIT在運行時編譯可以更好地利用profile-guided optimization。

答:也不一定。

看前面一個關於JIT和自適應動態編譯的傳送門,例如說CLR的JIT編譯器就用不上PGO,因爲它的編譯時機太早了,還沒來得及收集profile。

而對於AOT編譯的模型,也可以通過training run收集profile來做offline PGO,同樣可以得到PGO的好處。這種AOT編譯利用PGO的模型,跟JIT在運行時收集profile並做PGO的模型相比,最大的好處是AOT編譯是offline的,各種傳統優化可以做得更徹底;而缺點是如果training program跟實際應用的profile匹配度不高,或者實際應用在運行時行爲有phase shift,那這種offline的做法就無法很好的應對了。不過說真的,能高效應對phase shift的JIT系統也不多…

==============================================

最後純娛樂一下,放個截圖:

這是leetcode上的297。截圖是我用C寫的解的用時狀況。

我自己是做JVM的JIT編譯器的,我都無法理解爲啥這題會有那麼多Java submission的時間那麼短。感覺肯定是哪裏出錯了(不是開玩笑。要是有機會的話得問問leetcode的人看他們到底是怎麼跑Java的…

看評論區說不同語言的測試用例好像是不一樣的。原來有這種事?——在leetcode做Java時間統計的說所有語言的測試用例都是一樣的,但是Java的時間統計剪掉了一些前後的開銷。

現在私信我“資料”即可獲取Java工程化、高性能及分佈式、高性能、高架構。性能調優、Spring,MyBatis,Netty源碼分析和大數據等多個知識點高級進階乾貨的直播免費學習權限及領取相關資料

查看原文 >>
相關文章