Why Java Will Always Be Slower than C++
by Dejan Jelovic
為什么Java永遠比C++慢?
耍過Java程序,或者用Java碼過程序的人都曉得,Java要比用C++寫成的原生程序要慢。這是咱用Java時已經(jīng)承認的事實。
不過,很多人想要說服我們說這只不過是暫時的,他們說Java從設(shè)計上來講并不慢,相反,只是現(xiàn)在的JIT實現(xiàn)相對比較嫩,有很多能優(yōu)化的地方JIT并沒有優(yōu)化到,拖了后腿。其實不然,不管JIT們多牛,Java永遠要比C++慢。
我想說...
宣揚Java不慢于C++的人往往是覺得,(語法)嚴格的語言,可以讓編譯有更大的優(yōu)化空間。因此,除非你想做人肉編譯器優(yōu)化整個程序,否則通常都是編譯器做得更好。
這是真的。在數(shù)值計算領(lǐng)域,F(xiàn)ortran仍然勝于C++,的確因為它更嚴格。不用擔心指針瞎攪和,編譯器可以更安心地優(yōu)化。C++想打敗Fortran的唯一辦法,就是好好設(shè)計一個像Blitz++那樣的庫。
測試...
Java可以跟得上C++的地方,就是基準測試。計算起第N個斐波納契數(shù),或者運行起Linpack,Java沒理由不跟C++跑得一樣快。當所有的計算都放在一個類里,并且只使用基本的數(shù)據(jù)類型,比如說int或者double時,Java編譯器的確能跟得上C++的腳步。
事實...
當開始在程序中使用對象的時候,Java就放松了潛在的優(yōu)化。這一節(jié)會告訴你為什么。
1. 所有的對象都是從堆里分配的。
Java從棧里分配的,就只有基本數(shù)據(jù)類型,如int,或者double,還有對象的引用。所有的對象都是從堆里分配的。
當有大量語義上是一回事的對象時,這不成問題。C++同樣也是從堆上分配這些對象。但是,當有值語義不同的小對象時,這就是一個主要的性能殺手。
什么是小對象?對我來說,就是迭代器們。在設(shè)計中,我用了很多迭代器。別人可能會用復數(shù)。3D程序員可能會矢量或者點類。處理時間序列的人可能會有時間類。使用這些類的人,無一例外地討厭把不費時間的棧上分配換成花費固定時間的堆上分配。假如把它放在一個循環(huán)里,就變成了O(n)對0了。如果再加一層循環(huán),沒錯,又變成O(n^2)對0了。
2. 大量的轉(zhuǎn)換。
得益于模板,好的C++程序員甚至可以寫于完全沒有轉(zhuǎn)換的牛程序。不幸,Java沒有模板,所以Java代碼總是充滿了轉(zhuǎn)換。
對于性能,它們意味著什么?呃,在Java里所有的轉(zhuǎn)換都是很費時的動態(tài)轉(zhuǎn)換。多費時?想想你可能會怎么樣實現(xiàn)轉(zhuǎn)換的:
最快的方法就是,給每一個類賦值一個序號,然后用一個矩陣來描述任意兩個類是否相關(guān)的。如果是的話,需要給指針加上多少的位移才能進行轉(zhuǎn)換。這種方法的偽碼看起來應該是這樣的:
DestinationClass makeCast (Object o, Class destinationClass) {
Class sourceClass = o.getClass (); // JIT compile-time
int sourceClassId = sourceClass.getId (); // JIT compile-time
int destinationId = destinationClass.getId ();
int offset = ourTable [sourceClassId][destinationClassId];
if (offset != ILLEGAL_OFFSET_VALUE) {
return <object o adjusted for offset>;
}
else {
throw new IllegalCastException ();
}
}
好一堆代碼。這只是一個簡單的情景——用矩陣來表示類的關(guān)系浪費了一部分內(nèi)存,沒有哪個成熟的編譯器會這樣子做。他們會使用map或者遍歷繼承樹,這樣會變得更慢。
3. 攀升的內(nèi)存占用。
Java程序儲存數(shù)據(jù)占用的內(nèi)存大概是相當?shù)腃++程序的兩倍。原因如下:
1. 啟用了垃圾收集的程序一般都比不使用垃圾收集的程序多花50%的內(nèi)存。
2. 本來C++里在棧上分配的對象,到了Java就在堆上分配了。
3. Java對象比較大,因為所有的對象都有一個虛表,還要加上對(線程)同步的原生支持。
大的內(nèi)存映像讓程序更大概率被放到磁盤的交換區(qū)去。沒有什么比交換文件更慢的了。
4. 缺少更細致的控制。
Java原來就是作為一種簡單的語言來設(shè)計的。很多在C++里讓程序員控制細節(jié)的特性在Java里都被一腳踢開了。
比如說,在C++里可以改進引用的位置(?)。或者一次申請和釋放很多個對象。或者用指針耍一些小技巧,更快地訪問成員。
5. 沒有高層次的優(yōu)化。
程序員處理高層次的概念。而編譯器處理剩下的低層次概念。對于程序員來說,一個叫Matrix的類就代表了比一個叫Vector的類更高層次的概念。而對于編譯器來說,這些名字都是符號表的一個入口。他們只關(guān)心類里面有哪些函數(shù),函數(shù)里面有哪些語句。
這樣想一下,比如說要實現(xiàn)一個exp(double x, double y)函數(shù),計算出x的y次冪。對于一個編譯器,它能只看一下這個函數(shù),然后指出,exp(exp(x, 2), 0.5)可以優(yōu)化成x自己嗎?當然不行。
編譯器能做的優(yōu)化只是語句層面的,而y是在編譯器里面的。即使程序員知道兩個函數(shù)是對稱的,可以把它們都消去,或者函數(shù)的調(diào)用順序只是相反的,除非編譯器能只瞄一下語句,然后指出來,不然優(yōu)化是不可能完成的。
所以,如果想要完成一個高水平的優(yōu)化,必須存在某種方法,可以讓程序員來告訴編譯器優(yōu)化的規(guī)則。
沒有哪個流行的程序語言/系統(tǒng)可以做到這點,至少已知的方法,比如微軟承諾的智能語言,都不能。即便如此,在C++里可以用模板元編程來實現(xiàn)對高層次對象的優(yōu)化。臨時消除,部分求值,對稱函數(shù)調(diào)用的消去,和其它可以用模板實現(xiàn)的優(yōu)化。當然,不是所有的高層次優(yōu)化都可以這樣做。并且實現(xiàn)這些東西相當麻煩。但是大多數(shù)都可以完成,有人已經(jīng)用這些技術(shù)實現(xiàn)了好些時髦的庫。
不幸的是,Java沒有任何元編程的特質(zhì),因此在Java中不會有這種高層次的優(yōu)化。
所以...
由于存在這種語言特性,Java不可能達到C++這種速度。這相當程序上暗示了,對于要求高性能的軟件和競爭激烈的COTS舞臺上,使用Java不是一種明智的選擇。但是因為它和緩的學習曲線,它的容錯,和它龐大的標準庫,所以適合開發(fā)中小型自用和定制軟件。
附記...
1. 有人向James Gosling(誰?google之...)提交了很多可以改進Java性能的語言特性。文本在這里。不幸的是,Java語言已經(jīng)有四年沒有改動過了,所以看起來這些提議似乎不會在一夜之間被實現(xiàn)。
2. 最有可能往Java里加入泛型的是Generic Java。又很不幸的是,GJ只是通過在編譯時把所有類型信息去掉來支持泛型。所以最后面執(zhí)行環(huán)境看到的,仍然是緩慢的轉(zhuǎn)換。
3. 垃圾收集的FAQ包含了關(guān)于垃圾收集慢于定制分配器的信息(上面第四點)。
4. 這里是一篇宣稱垃圾收集比棧分配的快的文章。但是它的要求是物理內(nèi)存必須是程序?qū)嶋H需要的內(nèi)存的七倍之多。還有,它描述的是一種stop-and-copy(是不是那種執(zhí)行到一半,然后停下來,把內(nèi)存拷到另外一塊內(nèi)存,同時清除垃圾的那種方法?),而且還不是并發(fā)的。
反饋...
我收到很多關(guān)于這篇文章的反饋。附上一些典型的評論,還有我的回答:
“你還忘記了指出在Java里所有的方法都是虛方法,因為沒有人會加上final關(guān)鍵字。”
事實上,不使用final關(guān)鍵字不是問題的關(guān)鍵所在,使用者才是。同時,虛函數(shù)也沒有問題,但是卻失去了優(yōu)化機會。自從JIT們知道怎么樣內(nèi)聯(lián)虛函數(shù),這就變得不那么顯著了。
JIT可以內(nèi)聯(lián)虛函數(shù),所以Java可以比C++更快。
C++也可以使用JIT編譯。不信的可以看看.NET的C++編譯器。
到最后的時候,速度并不重要。電腦浪費了大部份時間在等待用戶輸入。
速度仍然很重要。我仍然在等我的筆記本啟動起來,我在等我的編譯器停下來,我還要等Word打開一個超長的文檔。
我在一個金融公司工作。有時候我必須對一個很大的數(shù)據(jù)集進行模擬。速度在這種情況下都很重要。
有些JIT可以在棧上分配一些對象。
當然,一些。
你的轉(zhuǎn)換代碼看起來很丑。可以在類的繼承層次上檢查類。
首先,這樣只比矩陣查找快一點點而已。
第二,這樣只能查找類,類只占多少?低層次的細節(jié)往往是通過接口來實現(xiàn)的。
哈,那么我們都應該使用匯編?
不是的,我們都要使用對業(yè)務有用的語言。Java提供了龐大的標準庫,讓很多任務變得容易,因此Java是偉大的。它比其它所有的語言更容易移植(但并非100%可移植——不同的平臺有不同問題)。它具有垃圾收集機制,簡化了內(nèi)存管理,同時也讓某些構(gòu)造如閉包可實現(xiàn)。
但是,同時,Java和所有的語言一樣,也有瑕疵。在值語義的類型上缺少支持。它的同步并不是很有效率。它的標準庫建立在異常檢查之上,把實現(xiàn)拖進了接口。它的性能可以更好。它的數(shù)學庫有些惱人的問題。諸如此類。
這些缺憾都是大問題嗎?看你用它做什么。因此,在幾種語言里,連同它的編譯器以及可以選擇的類庫里選擇對你的工程有利的一種。
回復 更多評論