有助於改善效能的Java程式碼技巧

紀莫發表於2019-05-31

前言

程式的效能受到程式碼質量的直接影響。這次主要介紹一些程式碼編寫的小技巧和慣例。雖然看起來有些是微不足道的程式設計技巧,卻可能為系統效能帶來成倍的提升,因此還是值得關注的。

慎用異常

在Java開發中,經常使用try-catch進行錯誤捕獲,但是try-catch語句對系統效能而言是非常糟糕的。雖然一次try-catch中,無法察覺到她對效能帶來的損失,但是一旦try-catch語句被應用於迴圈或是遍歷體內,就會給系統效能帶來極大的傷害。

以下是一段將try-catch應用於迴圈體內的示例程式碼:

    @Test
    public void test11() {

        long start = System.currentTimeMillis();
        int a = 0;
        for(int i=0;i<1000000000;i++){
            try {
                a++;
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        long useTime = System.currentTimeMillis()-start;
        System.out.println("useTime:"+useTime);

    }

上面這段程式碼執行結果是:

useTime:10

下面是一段將try-catch移到迴圈體外的程式碼,那麼效能就提升了將近一半。如下:

    @Test
    public void test(){
        long start = System.currentTimeMillis();
        int a = 0;
        try {
            for (int i=0;i<1000000000;i++){
                a++;
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        long useTime = System.currentTimeMillis()-start;
        System.out.println(useTime);
    }

執行結果:

useTime:6

使用區域性變數

呼叫方法時傳遞的引數以及在呼叫中建立的臨時變數都儲存在棧(Stack)中,速度快。其他變數,如靜態變數、例項變數等,都在堆(Heap)中建立,速度較慢。

下面是一段使用區域性變數進行計算的程式碼:

   @Test
    public void test11() {

        long start = System.currentTimeMillis();
        int a = 0;
        for(int i=0;i<1000000000;i++){
            a++;
        }
        long useTime = System.currentTimeMillis()-start;
        System.out.println("useTime:"+useTime);

    }

執行結果:

useTime:5

將區域性變數替換為類的靜態變數:

    static int aa = 0;
    @Test
    public void test(){
        long start = System.currentTimeMillis();

        for (int i=0;i<1000000000;i++){
            aa++;
        }
        long useTime = System.currentTimeMillis()-start;
        System.out.println("useTime:"+useTime);
    }

執行結果:

useTime:94

通過上面兩次的執行結果,可以看出來區域性變數的訪問速度遠遠高於類成員變數。

位運算代替乘除法

在所有的運算中,位運算是最為高效的。因此,可以嘗試使用位運算代替部分算術運算,來提高系統的執行速度。最典型的就是對於整數的乘除運算優化。

下面是一段使用算術運算的程式碼:

    @Test
    public void test11() {

        long start = System.currentTimeMillis();
        int a = 0;
        for(int i=0;i<1000000000;i++){
            a*=2;
            a/=2;
        }
        long useTime = System.currentTimeMillis()-start;
        System.out.println("useTime:"+useTime);
    }

執行結果:

useTime:1451

將迴圈體中的乘除運算改為等價的位運算,程式碼如下:

    @Test
    public void test(){
        long start = System.currentTimeMillis();
        int aa = 0;
        for (int i=0;i<1000000000;i++){
            aa<<=1;
            aa>>=1;
        }
        long useTime = System.currentTimeMillis()-start;
        System.out.println("useTime:"+useTime);
    }

執行結果:

useTime:10

上兩段程式碼執行了完全相同的功能,在每次迴圈中,都將整數乘以2,併除以2。但是執行結果耗時相差非常大,所以位運算的效率還是顯而易見的。

提取表示式

在軟體開發過程中,程式設計師很容易有意無意地讓程式碼做一些“重複勞動”,在大部分情況下,由於計算機的高速執行,這些“重複勞動”並不會對效能構成太大的威脅,但若希望將系統效能發揮到極致,提取這些“重複勞動”相當有意義。

比如以下程式碼中進行了兩次算術計算:

    @Test
    public void testExpression(){
        long start = System.currentTimeMillis();
        double d = Math.random();
        double a = Math.random();
        double b = Math.random();
        double e = Math.random();

        double x,y;
        for(int i=0;i<10000000;i++){
            x = d*a*b/3*4*a;
            y = e*a*b/3*4*a;
        }
        long useTime = System.currentTimeMillis()-start;
        System.out.println("useTime:"+useTime);

    }

執行結果:

useTime:21

仔細看能發現,兩個計算表示式的後半部分完全相同,這也意味著在每次迴圈中,相同部分的表示式被重新計算了。

那麼改進一下後就變成了下面的樣子:

    @Test
    public void testExpression99(){
        long start = System.currentTimeMillis();
        double d = Math.random();
        double a = Math.random();
        double b = Math.random();
        double e = Math.random();

        double p,x,y;
        for(int i=0;i<10000000;i++){
            p = a*b/3*4*a;
            x = d*p;
            y = e*p;
        }
        long useTime = System.currentTimeMillis()-start;
        System.out.println("useTime:"+useTime);
    }

執行結果:

useTime:11

通過執行結果我們可以看出來具體的優化效果。

同理,如果在某迴圈中需要執行一個耗時操作,而在迴圈體內,其執行結果總是唯一的,也應該提取到迴圈體外。

例如下面的程式碼:

for(int i=0;i<100000;i++){
    x[i] = Math.PI*Math.sin(y)*i;
}

應該改進成下面的程式碼:

//提取複雜,固定結果的業務邏輯處理到迴圈體外
double p = Math.PI*Math.sin(y);
for(int i=0;i<100000;i++){
    x[i] = p*i;
}

使用arrayCopy()

陣列複製是一項使用頻率很高的功能,JDK中提供了一個高效的API來實現它。

/**
     * @param      src      the source array.
     * @param      srcPos   starting position in the source array.
     * @param      dest     the destination array.
     * @param      destPos  starting position in the destination data.
     * @param      length   the number of array elements to be copied.
     * @exception  IndexOutOfBoundsException  if copying would cause
     *               access of data outside array bounds.
     * @exception  ArrayStoreException  if an element in the <code>src</code>
     *               array could not be stored into the <code>dest</code> array
     *               because of a type mismatch.
     * @exception  NullPointerException if either <code>src</code> or
     *               <code>dest</code> is <code>null</code>.
     */
    public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

如果在應用程式中需要進行陣列複製,應該使用這個函式,而不是自己實現。

下面來舉例:

    @Test
    public void testArrayCopy(){
        int size = 100000;
        int[] array = new int[size];
        int[] arraydest = new int[size];

        for(int i=0;i<array.length;i++){
            array[i] = i;
        }
        long start = System.currentTimeMillis();
        for (int k=0;k<1000;k++){
            //進行復制
            System.arraycopy(array,0,arraydest,0,size);
        }
        long useTime = System.currentTimeMillis()-start;
        System.out.println("useTime:"+useTime);
    }

執行結果:

useTime:59

相對應地,如果在程式中,自己實現陣列複製,其等價程式碼如下:

    @Test
    public void testArrayCopy99(){
        int size = 100000;
        int[] array = new int[size];
        int[] arraydest = new int[size];

        for(int i=0;i<array.length;i++){
            array[i] = i;
        }
        long start = System.currentTimeMillis();
        for (int k=0;k<1000;k++){
            for(int i=0;i<size;i++){
                arraydest[i] = array[i];
            }
        }
        long useTime = System.currentTimeMillis()-start;
        System.out.println("useTime:"+useTime);
    }

執行結果:

useTime:102

通過執行結果可以看出效果。

因為System.arraycopy()函式是native函式,通常native函式的效能要優於普通函式。僅出於效能考慮,在程式開發時,應儘可能呼叫native函式。

 

 

 

後面會持續更新。。。

相關文章