<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??碼云GVP開源項目 12k star Uniapp+ElementUI 功能強大 支持多語言、二開方便! 廣告
                > 本文是一篇介紹代碼性能的文章,由于筆者知識面有限,無法談及架構設計上的性能優化,只能描述一些`代碼層次`的優化方法。 [TOC] 談到性能優化,筆者認為沒有別的捷徑,唯一的辦法就是**測試,修改,再測試**。這里筆者使用 JMH(Java Microbenchmark Harness)來測試代碼的性能,它是一款微基準測試工具(`org.openjdk.jmh`,通過 Maven 引入即可)。 字符串(String),即字符數組(char[]),只要有可讀數據的傳輸,就有字符串的身影;雖然還有很多可優化的例子可以提及,但是本文只針對 **Java 字符串操作方法間的差異** 進行介紹。 同時,為了解釋清楚產生性能差異的原因,文中不得不貫穿一些 Java 前端編譯優化(javac)和后端編譯優化(JIT)的技術點,為了避免內容過泛,也無法具體地敘述。 ## 1 JMH 主要參數的含義 本節將通過一個demo,向讀者介紹 JMH 的常見參數。 以下是 3 個用于字符串格式化(format)操作的方法,讀者先根據經驗判斷一下哪個方法最快。 > 字符串格式化是比較常用的字符串操作,例如日志等,下面三個方法分別基于String#format,StringBuilder#append,MessageFormat#format實現 ```java @Benchmark public String byStringFormat() { return String.format("a: %s, b: %s, c: %s", a, b, c); } @Benchmark public String byStringBuilder() { return "a: " + a + ", b: " + b + ", c: " + c; } @Benchmark public String byMessageFormat() { return MessageFormat.format("a: {0}, b: {1}, c: {2}", a, b, c); } ``` 并且加入了"對照組",empty 方法會直接返回一個新的 String 實例: ```java @Benchmark public String empty() { return new String("a: 1234, b: 56.78, c: abcd"); } ``` ---- 以下是 JMH 的運行結果,結果表示: - 共測試了 4 個方法 - Mode=avgt,表示運行模式為平均運行時間(還可以設置為事務數) - Cnt表示迭代10次(默認每次迭代運行1秒) - Score和Error分別表示分數和誤差,單位是Units,即納秒/每操作 - 由于Mode=avgt,因此 Score 值越低,表示性能越好 Benchmark | Mode | Cnt | Score & Error | Units ---- | ---- | ---- | ----: | ---- StringFormatMethod.empty | avgt | 10 | 9.716 ± 0.256 | ns/op StringFormatMethod.byStringBuilder | avgt | 10 | 171.630 ± 2.517 | ns/op StringFormatMethod.byStringFormat | avgt | 10 | 1474.086 ± 36.624 | ns/op StringFormatMethod.byMessageFormat | avgt | 10 | 2946.144 ± 71.755 | ns/op 顯而易見,通過+號拼接實現的字符串格式化,性能遠遠高于 String#format 和 MessageFormat#format,原因如下: - javac 在處理+號拼接的操作時,new 出一個StringBuilder實例,對每個+號操作,依次調用其 StringBuilder#append 方法,連接各個 String 實例,最后調用 StringBuilder#toString 方法返回結果 - String#format 底層使用正則表達式,雖然已經提前編譯好了 Pattern ,但是模式匹配時,仍然引入了相當多的指令操作 - MessageFormat#format 底層雖然通過有限狀態機(說直白些,就是通過for和if)優化了解析模板和渲染結果的過程,但是需要考慮的數據類型太多,還是不可避免地引入了許多耗時操作 因此在沒有特殊的格式化需求(更具體地說,只有拼接字符串的需求),直接使用+號即可,例如: ```java Log.d("a: " + a + ", b: " + b + ", c: " + c); ``` ---- 本節最后,筆者將本次 JMH 輸出結果的前幾行拿到最后來介紹: - 前4行分別表示 JMH 版本號,Java虛擬機版本、目錄、參數 - Warmup 表示預熱時間,Measurement 表示方法運行時間;即10次迭代(iterations),每次1秒;測試結果都是 Measurement 中的 - Benchmark mode為平均運行時間,以及計算單位。 - Threads 為1,將同步執行 iterations ``` # JMH version: 1.19 # VM version: JDK 1.8.0_41, VM 25.40-b25 # VM invoker: /home/zhaoxuyang03/bin/jdk/java-se-8u41-ri/jre/bin/java # VM options: -javaagent:/home/zhaoxuyang03/bin/idea/lib/idea_rt.jar=35083:/home/zhaoxuyang03/bin/idea/bin -Dfile.encoding=UTF-8 # Warmup: 10 iterations, 1 s each # Measurement: 10 iterations, 1 s each # Timeout: 10 min per iteration # Threads: 1 thread, will synchronize iterations # Benchmark mode: Average time, time/op # Benchmark: net.zhaoxuyang.jmh.StringFormatMethod.byMessageFormat ``` ## 2 盡可能地減少內存申請操作 ### 2.1 字符串遍歷操作中,toCharArray 性能慢于 charAt 以下有兩種遍歷String的方法,toCharArray方法會通過String#toCharArray方法返回一個新的char[]實例,而charAt會通過str.charAt來遍歷String中的每個元素(charAt方法中含有try-catch語句): ``` /** JMH會為value中的每一項生成一組測試 */ @Param(value = {"short", "This is a long sentence..........................."}) String str; /** * 通過{@link String#toCharArray}方法生成char數組后,遍歷字符串 */ @Benchmark public int toCharArray() { char[] charArray = str.toCharArray(); int res = 0; for (int i = 0; i < charArray.length; i++) { res += charArray[i]; } return res; } /** * 在循環中通過調用charAt方法,遍歷字符串 */ @Benchmark public int charAt() { int res = 0; for (int i = 0; i < str.length(); i++) { res += str.charAt(i); } return res; } ``` 以下是基準測試結果: - toCharArray 方法的性能明顯地比 charAt 方法的差,主要體現在 `str.toCharArray` 開辟內存的耗時 - 雖然 charAt 中使用了 try-catch 包裹,但還是沒有申請內存的操作來得耗時,因此在使用中,是否可以考慮預先創建一個臨時的char[]數組,反復使用? Benchmark | (str) | Mode | Cnt | Score & Error | Units ----|----|----|----|----:|---- ToCharArrayOrCharAt.charAt | short | avgt | 10 | 8.118 ± 0.273 | ns/op ToCharArrayOrCharAt.charAt | This is a long sentence........................... | avgt | 10 | 23.122 ± 0.611 | ns/op ToCharArrayOrCharAt.toCharArray | short | avgt | 10 | 15.477 ± 1.176 | ns/op ToCharArrayOrCharAt.toCharArray | This is a long sentence........................... | avgt | 10 | 49.854 ± 2.164 | ns/op ### 2.2 通過預緩存來減少實際操作中的內存申請 本節會舉兩個例子,一個是 android.text.TextUtils#sTemp 字段,一個是目前許多模板引擎對 Integer#toString 方法的優化。 ---- 第一個例子是 android.text.TextUtils#sTemp 這個字段,提供給 TextUtils 內部使用,其主要目的就是為了減少內存申請操作,原理如下: - 每當方法體中需要申請 char[] 類型的局部變量時(例如 TextUtils#indexOf 方法),會調用 obtain(len) 方法返回一個臨時數組sTemp,長度不夠才重新申請內存 - 操作完成后,再調用設置 recycle 方法設置回 sTemp ``` /* package */ static char[] obtain(int len) { char[] buf; synchronized (sLock) { buf = sTemp; sTemp = null; } if (buf == null || buf.length < len) buf = ArrayUtils.newUnpaddedCharArray(len); return buf; } /* package */ static void recycle(char[] temp) { if (temp.length > 1000) return; synchronized (sLock) { sTemp = temp; } } ``` ---- 第二個例子是一個常見的優化手段,由于許多模板引擎會有整型轉字符串的需求,因此考慮到性能,在符合應用場景的前提下,會預先緩存 Integer#toString 的結果,實現方法如下: ``` /** * 提供toString方法的緩存工具類 */ public static class Util { /** 緩存范圍為 [0, CACHE_SIZE) */ private static final int CACHE_SIZE = 2048; /** 緩存內容 */ private static final String[] INT_CACHE; /* 預先生成toString結果 */ static { INT_CACHE = new String[CACHE_SIZE]; for (int i = 0; i < INT_CACHE.length; i++) { INT_CACHE[i] = Integer.toString(i); } } /** 不可實例化 */ private Util() { } /** * int轉String,超出緩存范圍則通過 {@link Integer#toString} 生成結果 */ public static String intToString(int i) { return (i >= 0 && i < CACHE_SIZE) ? INT_CACHE[i] : Integer.toString(i); } } ``` 也就是說當入參的范圍為[0, 2048)時,不再執行 Integer#toString 方法,直接返回預先計算的結果。 對于以下的測試用例: ``` /** * JMH會為value中的每一項生成一組測試 */ @Param(value = {"100", "1000", "10000"}) int value; /** * 無緩存的toString方法 */ @Benchmark public String nonCache() { return Integer.toString(value); } /** * 預緩存的toString方法 */ @Benchmark public String cache() { return Util.intToString(value); } ``` 其基準測試結果如下: - 當入參(value) 為 100 或 1000 時,可以走緩存,性能是未緩存時的8~9倍 - 當入參(value) 為 10000 時,未走緩存,性能與未緩存時持平(由于字節碼指令較多,必然稍遜于后者) Benchmark | (value) | Mode | Cnt | Score & Error | Units ----|----|----|----|----:|---- PreCache.cache | 100 | avgt | 10 | 4.303 ± 0.113 | ns/op PreCache.cache | 1000 | avgt | 10 | 4.255 ± 0.098 | ns/op PreCache.cache | 10000 | avgt | 10 | 36.420 ± 0.697 | ns/op PreCache.nonCache | 100 | avgt | 10 | 31.996 ± 7.915 | ns/op PreCache.nonCache | 1000 | avgt | 10 | 36.673 ± 7.452 | ns/op PreCache.nonCache | 10000 | avgt | 10 | 36.419 ± 1.337 | ns/op ## 3 使用語法糖時清楚實際運行的代碼 ### 3.1 不要使用+=來拼接字符串 Java 語法中,沒有操作符重載的概念,但是編譯器會為對象之間使用`+`號、`+=`號進行處理,下面只介紹 String 相關的運算符操作: - 通過`+`號連接的String(其他類型會通過 String#valueOf 方法轉換成String)實例,運行時會創建一個StringBuilder實例,通過append方法連接各String,最后通過 StringBuilder#toString 方法返回一個新實例 - 通過`+`號連接的String常量,編譯期間javac會直接將該表達式改為一個常量(`常量折疊`)。 - `a += b; a+=c; `操作會導致運行時創建一個StringBuilder對象連接a與b,再將toString結果賦值給a;再創建一個StringBuilder對象,連接a與c,再將toString結果賦值給a 以下是一個使用 `+=` 的bad case: ```java String a = "a"; String b = "b"; String c = "c"; /** * 通過+號拼接字符串 */ @Benchmark public String plus() { String res = a + b + c; return res; } /** * 通過StringBuilder的append方法拼接字符串 */ @Benchmark public String byStringBuilder() { String res = new StringBuilder().append(a).append(b).append(c).toString(); return res; } /** * 通過+=形式拼接字符串 */ @Benchmark public String plusEquals() { String res = a; a += b; a += c; return res; } ``` 其基準測試結果如下: - plusEquals 方法中使用了+=符號連接字符,雖然只進行了3個字符串實例的連接,但是其性能已經遠遠低于byStringBuilder和plus —— 如果放在一個長循環中使用,將造成更加嚴重的性能損耗 - 另一方面,可以看出 byStringBuilder 方法與 plus 方法性能相當 —— 其實兩個方法的字節碼完全一樣(`javap -c *.class`) Benchmark | Mode | Cnt | Score & Error | Units ----|----|----|----:|---- PlusOperator.byStringBuilder | avgt | 10 | 27.065 ± 1.003 | ns/op PlusOperator.plus | avgt | 10 | 27.178 ± 0.854 | ns/op PlusOperator.plusEquals | avgt | 10 | 318331.676 ± 68204.792 | ns/op ### 3.2 switch(String) 的代替方案 Java 7 中,switch塊里添加了對String類型的支持,例如: ```java /** 鍵 */ String key = "code_1"; /** * 通過switch(String)語法糖來選擇 * * @return {@link #key} 的匹配結果 */ @Benchmark public int bySwitch() { String key = this.key; switch (key) { case "code_0": return 0; case "code_1": return 1; case "code_2": return 2; default: return -1; } } ``` javac 解語法糖后,變成以下的等價形式: ``` /** * switch 解語法糖后的等價形式:通過 {@link String#hashCode} 與 {@link String#equals} 來選擇 * * @return {@link #key} 的匹配結果 */ @Benchmark public int bySwitchByteCode() { String key = this.key; int hashCode = key.hashCode(); switch (hashCode) { case -1355091362: // "code_0".hashCode() if ("code_0".equals(key)) { return 0; } case -1355091361: // "code_1".hashCode() if ("code_1".equals(key)) { return 1; } case -1355091360: // "code_2".hashCode() if ("code_2".equals(key)) { return 2; } default: return -1; } } ``` 通過 if 語句塊的等價實現如下: ```java /** * 通過 if 語句塊的等價實現 * * @return {@link #key} 的匹配結果 */ @Benchmark public int byIfEquals() { String key = this.key; if ("code_0".equals(key)) { return 0; } else if ("code_1".equals(key)) { return 1; } else if ("code_2".equals(key)) { return 2; } else { return -1; } } ``` 基于上述原理,可以預先計算"code_0"、"code_1"、"code_2"的hashCode,來實現代碼性能的提升,實現如下: ```java /** 預先計算好的"code_0"的hashCode */ final int PRE_HASH_CODE_0 = "code_0".hashCode(); /** 預先計算好的"code_1"的hashCode */ final int PRE_HASH_CODE_1 = "code_1".hashCode(); /** 預先計算好的"code_2"的hashCode */ final int PRE_HASH_CODE_2 = "code_2".hashCode(); /** * 通過預先計算 {@link String#hashCode()} 來選擇 * * @return {@link #key} 的匹配結果 */ @Benchmark public int byIfPreCache() { String key = this.key; int hashCode = key.hashCode(); if (hashCode == PRE_HASH_CODE_0 && "code_0".equals(key)) { return 0; } else if (hashCode == PRE_HASH_CODE_1 && "code_1".equals(key)) { return 1; } else if (hashCode == PRE_HASH_CODE_2 && "code_2".equals(key)) { return 2; } else { return -1; } } ``` 以下是基準測試結果: - bySwitchByteCode 比 bySwitch 稍快,是因為 javac 在編譯期提前算好了字符串常量的hashCode(因為switch語句塊中只能case常量),最后輸出了byteCode - byIfEquals 方法直接通過 equals 方法進行選擇,將直接進入 String#equals 方法中;而 byIfPreCache 中會先比較hashCode,避免了直接進入 String#equals 方法 - 另外,byIfPreCache 使用了提前計算好的 "code_1", "code_2","code_3" 的 hashCode,不用再在方法中重復計算,相比最初的 bySwitch 方法,提升了 33% 的性能 Benchmark | Mode | Cnt | Score & Error | Units ----|----|----|----:|---- SwitchString.byIfEquals | avgt |10 | 8.966 ± 0.172 | ns/op SwitchString.byIfPreCache | avgt | 10 | 4.842 ± 0.169 | ns/op SwitchString.bySwitch | avgt | 10 | 6.436 ± 0.142 | ns/op SwitchString.bySwitchByteCode | avgt | 10 | 6.108 ± 0.092 | ns/op ## 4 對 StringBuilder 的補充說明 ### 4.1 鏈式調用 StringBuidler#append 方法性能更好 本節主要建議(僅是一個建議)在方法內提前計算好局部變量的值,拼接字符串時,通過`append(str1).append(str2).append(str3).toStirng();` 的形式一次性輸出結果。 以下是測試用例,empty 方法為對照組,chainAppend 方法為鏈式調用,nonChainAppend 方法為非鏈式調用的形式。 ``` String a = "a"; int b = 10; char c = 'c'; boolean d = false; /** * 對照組 */ @Benchmark public String empty() { StringBuilder sb = new StringBuilder(); return sb.toString(); } /** * 鏈式調用append方法 */ @Benchmark public String chainAppend() { StringBuilder sb = new StringBuilder(); sb.append(a).append(b).append(c).append(d); return sb.toString(); } /** * 非鏈式調用append方法 */ @Benchmark public String nonChainAppend() { StringBuilder sb = new StringBuilder(); sb.append(a); sb.append(b); sb.append(c); sb.append(d); return sb.toString(); } ``` 以下是基準測試結果: - 結果表明,鏈式調用的性能略高于非鏈式調用 - 原因是這種非鏈式操作的append,每次都會從操作數棧中彈出,再從局部變量中裝載引用類型值入棧 Benchmark | Mode | Cnt | Score & Error | Units ----|----|----|----:|---- AppendMode.empty | avgt | 10 | 18.203 ± 2.581 | ns/op AppendMode.chainAppend | avgt | 10 | 51.673 ± 8.233 | ns/op AppendMode.nonChainAppend |avgt | 10 | 59.500 ± 18.410 | ns/op ### 4.2 StringBuffer 不比 StringBuidler 慢多少 StringBuffer 是線程安全的,而 StringBuilder 非線程安全;既然前者通過 synchronized 修飾了方法,性能必然沒StringBuilder好,但是其實沒有差多少。 以下 case 會分別對 StringBuffer 和 StringBuilder 的實例進行十、百、萬、百萬次字符串拼接操作: ``` @Param(value = {"10", "100", "10000", "1000000"}) int size; @Benchmark public String builder() { StringBuilder sb = new StringBuilder(); for (int i = 0; i < size; i++) { sb.append("name").append(i).append('\n'); } return sb.toString(); } @Benchmark public String buffer() { StringBuffer sb = new StringBuffer(); for (int i = 0; i < size; i++) { sb.append("name").append(i).append('\n'); } return sb.toString(); } ``` 以下是基準測試結果: - 結果表明在十至百萬次拼接中,StringBuffer的性能相對于StringBuilder性能,只低了1% ~ 6% - StringBuffer 跟 StringBuilder和相比性能并不差多少,得益于JIT C2階段的逃逸分析和鎖消除(對象只在方法內部使用,可以消除synchronized) - 逃逸分析:-XX:+DoEscapeAnalysis - 鎖消除:-XX:+EliminateLocks - 而實際上,方法內部局部變量以及方法參數是[線程私有](http://wiki.baidu.com/pages/viewpage.action?pageId=1312774340)的,即不存在線程安全問題,此時編譯器會直接提示開發者使用StringBuilder替換StringBuffer Benchmark |(size) | Mode | Cnt | Score & Error | Units ----|----:|----|----|----:|---- StringBuilderBuffer.buffer | 10 | avgt | 10 | 231.920 ± 5.211 | ns/op StringBuilderBuffer.buffer | 100 | avgt | 10 | 3655.676 ± 97.173 | ns/op StringBuilderBuffer.buffer | 10000 | avgt | 10 | 531097.767 ± 19279.096 | ns/op StringBuilderBuffer.buffer | 1000000 | avgt | 10 | 74592493.486 ± 1504365.581 | ns/op StringBuilderBuffer.builder | 10 | avgt | 10 | 228.170 ± 7.743 | ns/op StringBuilderBuffer.builder | 100 | avgt | 10 | 3275.142 ± 173.263 | ns/op StringBuilderBuffer.builder | 10000 | avgt | 10 | 492880.005 ± 7956.828 | ns/op StringBuilderBuffer.builder | 1000000 | avgt | 10 | 70098295.407 ± 1517437.435 | ns/op ## 附錄 ### 附錄A JMH 配置信息 ```java @BenchmarkMode(Mode.AverageTime) // 使用模式為運行時間,默認是Mode.Throughput,表示吞吐量 @Warmup(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS) // 預熱 @Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS) // 運行 @Threads(1) // 同時執行的線程數 @Fork(1) // 為每個方法啟動一個進程 @OutputTimeUnit(TimeUnit.NANOSECONDS) // 統計結果的時間單元 @State(Scope.Benchmark) // 對象的生命周期 public class BenchmarkTest { public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder() .include(MethodHandles.lookup().lookupClass().getSimpleName()) .forks(1) .build(); new Runner(opt).run(); } } ```
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看