以程式碼角度最佳化

這邊僅簡單列出一些小tips,其背後的原理,佐證可參考以下書單:

  1. 我的Java程式比你的快10倍 : 從概念到工具的極度優化 \/ 葛一鳴著

  2. Android高效能多執行緒 \/ Anders Göransson著 ; 楊仁和譯

欲提高App效能,最基本也是做簡單的辦法,就是諄照 http:\/\/developer.android.com\/training\/articles\/perf-tips.html 上所建議的小技巧。記住這些技巧,提升coding的performance。養成這些coding的習慣不外乎就是為了減少前言所歸納的:避免memory leak、減少gc過度呼叫,...等等。不要做不需要做的事避免配置記憶體。

  • 使用StringBuffer取代String。(String也是透過StringBuffer所實作的)

    • 有必要時,也可以使用String.format取代String串聯的問題。(連結)
  • 如果需獲取字串(String)中的子字串(Sub-String),請直接從原字串呼叫substring函數做處理,而不用額外new一個重複的object。

      public String get50Str(String data) {
          return (data.length() > 50)? data.substring(0,50):data;`
      }
    
  • 舉個例子,假設你有一個回傳String的函式,且回傳的值是一定被append到StringBuffer的話,就不要return來建立多餘的short-lived temporary object,直接傳StringBuffer 的 reference在函式裏面動作即可。

      public void append(StringBuffer sb,String apStr) {
          sb.append(apStr);`
      }
    
  • 盡量使用一維陣列;假設需用到多維陣列,且請各個切割成多個醫為陣列來做為儲存容器。

  • 用int宣告的陣列會比Integer的陣列來的有效率

    • 因為int是屬於primitive type(基本型態),Integer是class(類別)。(牽涉到Autoboxing和Unboxing概念)。物件需要宣告實例化,且使用到cache memory後,需要GC memory,這些都是要花時間的的;所以越少GC,效能就越好
    • 同理float比Float,double對比Double。
  • 但是在Android中,不推薦使用float-point 做運算,同樣情況下,int比float快兩倍。

    • 雖然double所使用的空間是float的2倍多,但這可以忽略。若要使用浮點運算,double還是較好。
    • 即使是整數,有些硬體只支援乘法,不支援除法運算。所以除法和負數運算可以以軟體實作,屆時你可能需要一個hash table和大量數學算式。
  • 在傳遞兩個一維陣列比傳遞一個陣列附帶兩個reference object來的有效率。但要在可讀性上做些妥協。

      class Object() {
    
          int Foo;
    
          int Bar;
    
        }
    
      ArrayList<Object> twoElement = .....; (沒效率)
    
      ///////////////////我是分隔線////////////////////
    
      ArrayList<int> aElement = ...;
    
      ArrayList<int> bElement = ...; (有效率)
    
  • 使用static比virtual快15-20%。
  • 使用**static final**宣告常數

    • 因為被宣告為static final的常數,在app第一次被使用時,compiler會直接把變數名稱和對映的值寫到dex file中。在後續被使用時,會直接透過dex file回傳對映的value,減少memory access的時間,提升效能。
    • 如果是宣告成static,會使用到memory cahce,後續有無法GC memory,進一步造成頻繁GC或是memory leak,進一步導致效能下降。
  • 避免使用static宣告。

    • 因為static的生命週期過長,使用不當導致memory leak。
  • 避免使用getter和setter。

    • 原因是在C++等native language中,常有getter的寫法,而不是直接去access value,因為這是C++優良的好習慣,但是Java或是C#物件島享的語言,也直接繼承這樣的寫法。然而C++有JIT(Just In Time Compiler)功能可以幫助code做性能優化功能,例如 i = getCount() 透過JIT可簡化為 i = mCount,使程式在執行時效能提高;畢竟直接access value比呼叫函式(virtual method)再取得回傳value來的快。這樣的最佳化功能在JIT只是一小部分,稱之為 inline the access。

      • 有JIT,直接使用(inline the access) 會比getter快7倍。

        • 因為JIT不只 inline the access 功能,還有...(JIT詳細介紹可參考[2])
      • 沒有 JIT 直接使用(inline the access) 會比getter快3倍。

      • 但是如果你有使用 ****ProGuard*\***, 就不用擔心此問題**。因為 ProGuard幫你做 inline accessors。

  • 在普通的情況下,使用for-each效能比使用for來的好。

    • static class Foo {

      int mSplat;

      }

      Foo[] mArray = ...

      public void zero() {

      int sum = 0;

      for (int i = 0; i < mArray.length; ++i) {

      sum += mArray[i].mSplat;

      } }

      \/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/ public void one() {

      int sum = 0;

      Foo[] localArray = mArray;

      int len = localArray.length;

      for (int i = 0; i < len; ++i) {

      sum += localArray[i].mSplat;

      } }

      \/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/ public void two() {

      int sum = 0;

      for (Foo a : mArray) {

      sum += a.mSplat;

      } }

    • zero()是最慢的,因為JIT沒有辦法對它做最佳化。

      one()稍微快些。

      two() 在沒有做JIT時是最快的,可是如果經過JIT之後,與方法one()是差不多一樣快的。它使用了增強的循環方法for-each。

      所以請儘量使用for-each的方法,但是對於ArrayList,請使用方法one()

  • Consider Package Instead of Private Access with Private Inner Classes

    • 不太懂其意思,等待好心人解釋。
  • 建議使用系統內建的library method,其中 System.arraycopy()) 是手寫 loop的9倍快。

  • 使用Native Code要小心,有些 Native method無法被JIT最佳化。

  • 避免使用Enum。運行時,會額外產生memory access,不可忽視。[3]

    • 假如我們有一個基本的app,經過compiler後,dex大小是2556bytes,在這基準上,增加以下code,以final static常數作為判斷比較值:

      • 增加以下code經過compiler後,dex是2680 bytes,對照原本的code,只增加124bytes。
    • 假如是使用enum的情況呢,會是如何?

      • 使用enmu後的dex大小是4188 bytes,對照一開始的codey足足增加了1632 bytes(4188-2556),增長量是使用static int的13倍。
    • 不僅僅如此,使用enum,運行時還會產生額外的memory 空間佔用,如下圖所示。

    • 解決辦法:

      • 【方法一】使用IntDef annotation改寫
      • 【方法二】使用ProGuard會自動壓縮enum,使其最佳化。
  • 慎選資料容器。例如HashMap .ArrayMap SparaseArray...

部分範例參考至 ref:

http:\/\/leehom59.blogspot.tw\/2011\/02\/basic-concept-about-java-coding-in.html

[2]http:\/\/blog.dontcareabout.us\/2013\/03\/jit-compiler.html

JIT

  1. 還沒作最佳化的程式碼:

    class Calculator {
    Wrapper wrapper;
    public void calculate() {
    y = wrapper.get();
    z = wrapper.get();
    sum = y + z;
    }
    }
    
    class Wrapper {
    final int value;
    final int get() {
    return value;
    }
    }
    
  2. 最佳化過後的程式碼:

    class Calculator {
    Wrapper wrapper;
    public void calculate() {
    y = wrapper.value;
    sum = y + y;
    }
    }
    
    class Wrapper {
    final int value;
    final int get() {
    return value;
    }
    }
    

第一個 class 是開發人員寫的例子,第二個是 JIT 處理過後的樣本。 這裡頭包含了幾種最佳化的技術。讓我們來看一下最終結果是怎麼做到的:

  1. 還沒最佳化的程式碼。在還沒被判定成 hot spot 時,程式碼會這樣執行:

    class Calculator {
    Wrapper wrapper;
    public void calculate() {
    y = wrapper.get();
    z = wrapper.get();
    sum = y + z;
    }
    }
    
  2. inline method:用 b.value 取代 wrapper.get(), 不透過函數呼叫而直接存取 wrapper.value 來減少延遲。

    class Calculator {
    Wrapper wrapper;
    public void calculate() {
    y = wrapper.value;
    z = wrapper.value;
    sum = y + z;
    }
    }
    
  3. 移除多餘的載入:用 z = y 取代 z = wrapper.value, 所以只存取區域變數而不是 wrapper.value 來減少延遲。

    class Calculator {
    Wrapper wrapper;
    public void calculate() {
    y = wrapper.value;
    z = y;
    sum = y + z;
    }
    }
    
  4. copy propagation:用 y = y 取代 z = y, 沒有必要再用一個變數 z,因為 zy 會是相等的。

    class Calculator {
    Wrapper wrapper;
    public void calculate() {
    y = wrapper.value;
    y = y;
    sum = y + y;
    }
    }
    
  5. 消除不用的程式碼:y = y 是不必要的,可以消滅掉。

    class Calculator {
    Wrapper wrapper;
    public void calculate() {
    y = wrapper.value;
    sum = y + y;
    }
    }
    

results matching ""

    No results matching ""