優(yōu)化Java中的多態(tài)代碼 走進(jìn)新版OpenJDK
優(yōu)化Java中的多態(tài)代碼
Oracle的Java是一個(gè)門(mén)快速的語(yǔ)言,有時(shí)候它可以和C++一樣快。編寫(xiě)Java代碼時(shí),我們通常使用接口、繼承或者包裝類(wrapper class)來(lái)實(shí)現(xiàn)多態(tài),使軟件更加靈活。不幸的是,多態(tài)會(huì)引入更多的調(diào)用,讓Java的性能變得糟糕。部分問(wèn)題是,Java不建議使用完全的內(nèi)聯(lián)代碼,即使它是非常安全的。(這個(gè)問(wèn)題可能會(huì)在***的Java版本里得到緩解,請(qǐng)看文章后面的更新部分)
考慮下這種情況,我們要用接口抽象出一個(gè)整型數(shù)組:
- public interface Array {
- public int get(int i);
- public void set(int i, int x);
- public int size();
- }
你為什么要這樣做?可能是因?yàn)槟愕臄?shù)據(jù)是保存在數(shù)據(jù)庫(kù)里、網(wǎng)絡(luò)上、磁盤(pán)上或者在其他的數(shù)據(jù)結(jié)構(gòu)里。你想一次編碼后就不用關(guān)心數(shù)組的具體實(shí)現(xiàn)。
編寫(xiě)一個(gè)與標(biāo)準(zhǔn)Java數(shù)組一樣高效率的類并不難,不同之處在于它實(shí)現(xiàn)了這個(gè)接口:
- public final class NaiveArray implements Array {
- protected int[] array;
- public NaiveArray(int cap) {
- array = new int[cap];
- }
- public int get(int i) {
- return array[i];
- }
- public void set(int i, int x) {
- array[i] = x;
- }
- public int size() {
- return array.length;
- }
- }
至少在理論上,NaiveArray類不會(huì)出現(xiàn)任何的性能問(wèn)題。這個(gè)類是final的,所有的方法都很簡(jiǎn)短。
不幸的是,在一個(gè)簡(jiǎn)單的benchmark類里,當(dāng)使用NavieArray作為數(shù)組實(shí)例時(shí),你會(huì)發(fā)現(xiàn)NavieArray比標(biāo)準(zhǔn)數(shù)組慢5倍以上。就像這個(gè)例子:
- public int compute() {
- for(int k = 0; k < array.size(); ++k)
- array.set(k,k);
- int sum = 0;
- for(int k = 0; k < array.size(); ++k)
- sum += array.get(k);
- return sum;
- }
你可以通過(guò)使用NavieArray作為NavieArray的一個(gè)實(shí)例來(lái)稍微減緩性能問(wèn)題(避免使用多態(tài))。不幸的是,它依然會(huì)慢3倍多。而你僅是放棄了多態(tài)的好處。
那么,強(qiáng)制使用內(nèi)聯(lián)函數(shù)調(diào)用會(huì)怎樣?
一個(gè)可行的解決方法是手動(dòng)實(shí)現(xiàn)內(nèi)聯(lián)函數(shù)。你可以使用 instanceof 關(guān)鍵字來(lái)提供優(yōu)化實(shí)現(xiàn),否則你只會(huì)得到一個(gè)普通(更慢)的實(shí)現(xiàn)。例如,如果你使用下面的代碼,NavieArray就會(huì)變得和標(biāo)準(zhǔn)數(shù)組一樣快:
- public int compute() {
- if(array instanceof NaiveArray) {
- int[] back = ((NaiveArray) array).array;
- for(int k = 0; k < back.length; ++k)
- back[k] = k;
- int sum = 0;
- for(int k = 0; k < back.length; ++k)
- sum += back[k];
- return sum;
- }
- //...
- }
當(dāng)然,我也會(huì)介紹一個(gè)維護(hù)問(wèn)題作為需要實(shí)現(xiàn)不止一次的同類算法…… 當(dāng)出現(xiàn)性能問(wèn)題時(shí),這是一個(gè)可接受的替代。
和往常一樣,我的benchmarking代碼可以在網(wǎng)上獲取到。
總結(jié)
- 一些Java版本可能不完全支持頻繁的內(nèi)聯(lián)函數(shù)調(diào)用,即使它可以并且應(yīng)該支持。這會(huì)造成嚴(yán)重的性能問(wèn)題。
- 把類聲明為 final 看起來(lái)不會(huì)緩解性能問(wèn)題。
- 對(duì)于消耗大的函數(shù),可行的解決方法是自己手動(dòng)優(yōu)化多態(tài)和實(shí)現(xiàn)內(nèi)聯(lián)函數(shù)調(diào)用。使用 instanceof 關(guān)鍵字,你可以為一些特定的類編寫(xiě)代碼并且(因此)保留多態(tài)的靈活性。
更新
Erich Schubert使用 double 數(shù)組運(yùn)行簡(jiǎn)單的benchmark類發(fā)現(xiàn)他的運(yùn)行結(jié)果與我的結(jié)果相矛盾,而且我們的變量實(shí)現(xiàn)都是一樣的。我通過(guò)更新到***版本的OpenJDK證明了他的結(jié)果。下面的表格給出了處理10百萬(wàn)整數(shù)需要的納秒時(shí)間:
Function | Oracle JDK 8u11 | OpenJDK 1.8.0_40 | OpenJDK 1.7.0_65 |
---|---|---|---|
straight arrays | 0.92 | 0.71 | 0.87 |
with interface | 5.9 | 0.70 | 6.3 |
with manual inlining | 0.98 | 0.71 | 0.93 |
正如我們看到的,***版本的OpenJDK十分智能,并且消除了多態(tài)的性能開(kāi)銷(xiāo)(1.8.0_40)。如果你足夠幸運(yùn)地在使用這個(gè)JDK,你不需要擔(dān)心這 篇文章所說(shuō)的性能問(wèn)題。但是,這個(gè)總體思想依然值得應(yīng)用在更復(fù)雜的場(chǎng)景里。例如,JDK優(yōu)化可能依然達(dá)不到你期待的性能要求。
原文鏈接: lemire 翻譯: ImportNew.com - 進(jìn)林