(點選上方公眾號,可快速關註)
來源:ImportNew – 進林
最佳化Java中的多型程式碼
Oracle的Java是一個門快速的語言,有時候它可以和C++一樣快。編寫Java程式碼時,我們通常使用介面、繼承或者包裝類(wrapper class)來實現多型,使軟體更加靈活。不幸的是,多型會引入更多的呼叫,讓Java的效能變得糟糕。部分問題是,Java不建議使用完全的行內程式碼,即使它是非常安全的。(這個問題可能會在最新的Java版本里得到緩解,請看文章後面的更新部分)
考慮下這種情況,我們要用介面抽象出一個整型陣列:
public interface Array {
public int get(int i);
public void set(int i, int x);
public int size();
}
你為什麼要這樣做?可能是因為你的資料是儲存在資料庫裡、網路上、磁碟上或者在其他的資料結構裡。你想一次編碼後就不用關心陣列的具體實現。
編寫一個與標準Java陣列一樣高效率的類並不難,不同之處在於它實現了這個介面:
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類不會出現任何的效能問題。這個類是final的,所有的方法都很簡短。
不幸的是,在一個簡單的benchmark類裡,當使用NavieArray作為陣列實體時,你會發現NavieArray比標準陣列慢5倍以上。就像這個例子:
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;
}
你可以透過使用NavieArray作為NavieArray的一個實體來稍微減緩效能問題(避免使用多型)。不幸的是,它依然會慢3倍多。而你僅是放棄了多型的好處。
那麼,強制使用行內函式呼叫會怎樣?
一個可行的解決方法是手動實現行內函式。你可以使用 instanceof 關鍵字來提供最佳化實現,否則你只會得到一個普通(更慢)的實現。例如,如果你使用下麵的程式碼,NavieArray就會變得和標準陣列一樣快:
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;
}
//…
}
當然,我也會介紹一個維護問題作為需要實現不止一次的同類演演算法…… 當出現效能問題時,這是一個可接受的替代。
和往常一樣,我的benchmarking程式碼可以在網上獲取到。
總結
-
一些Java版本可能不完全支援頻繁的行內函式呼叫,即使它可以並且應該支援。這會造成嚴重的效能問題。
-
把類宣告為 final 看起來不會緩解效能問題。
-
對於消耗大的函式,可行的解決方法是自己手動最佳化多型和實現行內函式呼叫。使用 instanceof 關鍵字,你可以為一些特定的類編寫程式碼並且(因此)保留多型的靈活性。
更新
Erich Schubert使用 double 陣列執行簡單的benchmark類發現他的執行結果與我的結果相矛盾,而且我們的變數實現都是一樣的。我透過更新到最新版本的OpenJDK證明瞭他的結果。下麵的表格給出了處理10百萬整數需要的納秒時間:
正如我們看到的,最新版本的OpenJDK十分智慧,並且消除了多型的效能開銷(1.8.0_40)。如果你足夠幸運地在使用這個JDK,你不需要擔心這 篇文章所說的效能問題。但是,這個總體思想依然值得應用在更複雜的場景裡。例如,JDK最佳化可能依然達不到你期待的效能要求。
看完本文有收穫?請轉發分享給更多人
關註「ImportNew」,提升Java技能