Android開發效能篇--fork/join
在實際的開發過程中,大家都會注意到不在UI執行緒中去做IO或複雜業務處理,卻往往忽視了在效能方面的優化。在Android開發過程中只是區分UI執行緒和非UI執行緒只能解決UI無響應(ANR)的問題,但是還是對程式或者某個業務模組的效能的提升卻是了了,具體表現形式就是菊花
時間長。之所以出現這種現象是因為我們在實際的開發過程中沒有充分的利用現代CPU的多核心的特性,白白的浪費了處理器的效能,造成了這種一核有難,多核圍觀局面。
比如在一個大小為N(N>1000000)的集合中進行M次查詢,常規解決方案可能就是下面的示例了:
public static long[] SOURCE = new long[MAX_LEN]; private static long[] KEY = new long[80]; static void normalSearch() { long startTime = System.currentTimeMillis(); List<Integer> result = new ArrayList<>(); for (long k : KEY) { for (int i = 0; i < SOURCE.length; i++) { if (SOURCE[i] == k) { result.add(i); } } } System.out.println("\n耗時:" + (System.currentTimeMillis() - startTime)); result.sort(Comparator.comparingInt((i) -> i)); for (Integer integer : result) { System.out.print(integer + ","); } } public static void main(String[] args) { //生成隨機數 for (int i = 0; i < SOURCE.length; i++) { SOURCE[i] = (long) (Math.random() * MAX_LEN); } //生成帶查詢的隨機key for (int i = 0; i < KEY.length; i++) { KEY[i] = (long) (Math.random() * MAX_LEN); } System.out.println("可用CUP核心數目:" + Runtime.getRuntime().availableProcessors()); normalSearch(); }
執行結果如下:
可用CUP核心數目:8 耗時:1595 119661,337105,401423,420279,582259,833772,915289,1220176,1362115,1385498,1407456,1410458,1412537,1486334,1672986,1674490,1856904,1897285,2102246,2252358,2346380,2432449,2682087,2912521,3092778,3264722,3357749,3608071,3689355,3734622,3744433,4125270,4234587,4281248,4314317,4341244,4436954,4677349,4753986,4813957,4910578,4958269,5015601,5080880,5180586,5300020,5364204,5631064,5756371,5878736,5996476,6036646,6066038,6297594,6393639,6408677,6415918,6664395,6858922,7025579,7297229,7705758,7815756,7913880,7940796,7971198,8160302,8187878,8258721,8273061,8478081,8584189,8616914,8650043,8695414,8815420,8985573,9039380,9079216,9226196,9304474,9422083,9465702,9545556,9741503,9770754,9848830,
如果說這是在一個8核心CPU機器上那有7個核心的CPU被浪費了,現在Android裝置的CPU基本都是4核心起步,如果程式全部使用上述方案去設計實現那程式的效能就被認為的設定了瓶頸。既然發現了問題,那就想解決方案,解決方案也很容易想到那就是使用多執行緒,將查詢業務拆分成多個子任務然後分別放到獨立的執行緒中執行,最終將所有自任務的執行結果彙總起來。從Java 7開始JDK添加了分支/合併(fork/join) Api,我們除了可以使用傳統的執行緒(Thread | Runnable)去實現平行計算外還可以用 fork/join api去實現而且更方便。這裡我們將整個查詢任務按照20個每組的策略拆成四個子任務(這裡可以根據當前裝置的CPU核心數量作為任務拆分數量的依據,達到充分利用CPU資源的目的)分別在各自的執行緒中執行,最終將子任務的結果彙總生成最終結果。
實現方案如下:
static class SearchTask extends RecursiveTask<List<Integer>> { private static final int SLOP = 80 / 4; private long[] data; public SearchTask(long[] data) { this.data = data; } @Override protected List<Integer> compute() { if (data.length <= SLOP) { return doSearch(data); } ForkJoinTask<List<Integer>> fork = new SearchTask(Arrays.copyOfRange(data, SLOP, data.length)).fork(); List<Integer> result = new SearchTask(Arrays.copyOf(data, SLOP)).compute(); result.addAll(fork.join()); return result; } private List<Integer> doSearch(long[] ds) { List<Integer> result = new ArrayList<>(); for (long d : ds) { for (int i = 0; i < SOURCE.length; i++) { if (d == SOURCE[i]) { result.add(i); } } } return result; } }//end SearchTask static void parallelSearch() { long startTime = System.currentTimeMillis(); List<Integer> result = new ForkJoinPool().invoke(new SearchTask(KEY)); System.out.println("\n耗時:" + (System.currentTimeMillis() - startTime)); result.sort(Comparator.comparingInt((i) -> i)); for (Integer integer : result) { System.out.print(integer + ","); } } public static void main(String[] args) { //生成隨機數 for (int i = 0; i < SOURCE.length; i++) { SOURCE[i] = (long) (Math.random() * MAX_LEN); } //生成帶查詢的隨機key for (int i = 0; i < KEY.length; i++) { KEY[i] = (long) (Math.random() * MAX_LEN); } System.out.println("可用CUP核心數目:" + Runtime.getRuntime().availableProcessors()); parallelSearch(); }
執行結果如下:
可用CUP核心數目:8 耗時:229 190450,518321,519536,667571,1026180,1236306,1275936,1374522,1393754,1576010,1616249,1616646,1875229,1885370,2023697,2039008,2098160,2156943,2293885,2386163,2512508,2633884,2844610,3008867,3172781,3260755,3289703,3306916,3617237,3926288,4029758,4100177,4308783,4427440,4571876,4737692,4937399,4998991,5039259,5042379,5085831,5210899,5331303,5358580,5389430,5714091,5742347,5847322,6005706,6050434,6616903,6879677,7169505,7229141,7248784,7318644,7536698,7605990,7656189,8110381,8126203,8142227,8366379,8426563,8496790,8552683,8584329,8671821,8713540,8991753,9005534,9123005,9197396,9206506,9268363,9472202,9515304,9541788,9669959,9679572, Process finished with exit code 0
從結果可以看出使用多工拆分策略後程序的執行效能得到了大幅的提升,但是並不是在所有的場景下使用任務拆分都能提高效能,我整理了一份表格如下:
M | 執行緒數量 | N | 執行時間(序列)/ms | 執行時間(並行)/ms |
---|---|---|---|---|
80 | 4 | 1000 | 2 | 4 |
80 | 4 | 10000 | 6 | 16 |
80 | 4 | 100000 | 30 | 30 |
80 | 4 | 1000000 | 173 | 50 |
80 | 4 | 10000000 | 1579 | 229 |
通過上表結果,我們可以得出結論在保持查詢次數M和拆分任務數量不變的情況下,查詢集合N越大效能提升越明顯,因為任務執行緒的切換和同步也是需要耗費時間的,當查詢任務比較小(N比較小的情況下)使用任務拆分的策略並不能帶來效能的提升,反而會是效能下降,所以在使用的時候需要特別注意。