歡迎光臨
每天分享高質量文章

最佳化 Java 中的多型程式碼

(點選上方公眾號,可快速關註)


來源: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技能

贊(0)

分享創造快樂