很多講Java優化的文章都會強調對String拼接的優化。倒不用特意記,本質上在於對不可變類優勢和劣勢的理解上。
需要關注的是編譯器對String拼接做出的優化,在簡單場景下的效能能夠與StringBuilder相當,複雜場景下仍然有較大的效能問題。網上關於這一問題講的非常亂;如果我講的有什麼紕漏,也歡迎指正。
JDK版本:oracle java 1.8.0_102
本文用到了反編譯工具jad。在查閱網上關於String拼接操作的優化時發現了這個工具,能同時反編譯出來原始碼和位元組碼,親測好用,點我下載。
String拼接的效能問題
優化之前,每次用”+”拼接,都會生成一個新的String。特別在迴圈拼接字串的場景下,效能損失是極其嚴重的:
- 空間浪費:每次拼接的結果都需要建立新的不可變類
- 時間浪費:建立的新不可變類需要初始化;產生大量“短命”垃圾,影響 young gc甚至full gc
所謂簡單場景
簡單場景和複雜場景是我亂起的名字,幫助理解編譯器的優化方案。
簡單場景可理解為在一句中完成拼接:
int i = 0;
String sentence = “Hello” + “world” + String.valueOf(i) + “
”;
System.out.println(sentence);複製程式碼
利用jad可看到優化結果:
int i = 0;
String sentence = (new StringBuilder()).append(“Hello”).append(“world”).append(String.valueOf(i)).append(“
”).toString();
System.out.println(sentence);複製程式碼
是不是很神奇,竟然把String的拼接操作優化成了StringBuilder#append()!
此時,可以認為已經將簡單場景的空間效能、時間效能優化到最優(僅針對String拼接操作而言),看起來編譯器已經完成了必要的優化。你可以測試一下,簡單場景下的效能能夠與StringBuilder相當。但是——“但是”以前的都是廢話——編譯器的優化對於複雜場景的幫助卻很有限了。
所謂複雜場景
所謂複雜場景,可理解為“編譯器不確定(或很難確定,於是不做分析)要進行多少次字串拼接後才需要轉換回String”。可能表述不準確,理解個大概就好。
我們分析一個最簡單的複雜場景:
String sentence = “”;
for (int i = 0; i < 10000000; i++) {
sentence += “Hello” + “world” + String.valueOf(i) + “
”;
}
System.out.println(sentence);複製程式碼
理想的優化方案
當然,無論什麼場景,程式猿都可以手動優化:
- 在效能敏感的場景使用StringBuilder完成拼接。
- 在效能不敏感的場景使用更方便的String。
PS:別吐槽,這樣的API設計是合理的,在合適的地方做合適的事。
理想目標是把這件事交給javac和JIT:
- 設定一個拼接次數的閾值,超過閾值就啟動優化(對於javac有一個編譯期的閾值,JIT有一個執行期的閾值,以分階段優化)。
- 優化時,在拼接前生成StringBuilder物件,將拼接操作換成StringBuilder#append(),繼續使用該物件,直至“需要”String物件時,使用StringBuilder#toString()“懶載入”新的String物件。
該優化方案的難度在於程式碼分析:機器很難知道到底何時“需要”String物件,所以也很難在合適的位置注入程式碼完成“懶載入”。
雖然很難實現,但還是給出理想的優化結果,以供實際方案對比:
String sentence = “”;
StringBuilder sentenceSB = new StringBuilder(sentence);
for (int i = 0; i < 10000000; i++) {
sentenceSB.append(“Hello”).append(“world”).append(String.valueOf(i)).append(“
”);
}
sentence = sentenceSB.toString();
System.out.println(sentence);複製程式碼
實際的優化方案
利用jad檢視實際的優化結果:
String sentence = “”;
for (int i = 0; i < 10000000; i++) {
sentence = (new StringBuilder()).append(sentence).append(“Hello”).append(“world”).append(String.valueOf(i)).append(“
”).toString();
}
System.out.println(sentence);複製程式碼
可以看到,實際上編譯器的優化只能達到簡單場景的最優:僅優化字串拼接的一句。這種優化程度,對於上述複雜場景的效能提升很有限,迴圈時還是會生成大量短命垃圾,特別是字串拼接到很大的時候,空間和時間上都是致命的。
通過對理想方案的分析,我們也能理解編譯器優化的無奈之處:編譯器無法(或很難)通過程式碼分析判斷何時是最晚進行懶載入的時機。為什麼呢?我們將程式碼換個形式可能更容易理解:
String sentence = “”;
for (int i = 0; i < 10000000; i++) {
sentence = sentence + “Hello” + “world” + String.valueOf(i) + “
”;
}
System.out.println(sentence);複製程式碼
觀察第3行的程式碼,等式右側引用了sentence。我肉眼知道這句話只完成了字串拼接,機器呢?最起碼,現在的機器還很難通過程式碼判斷。
待以後將人工智慧與編譯優化結合起來,就算只能以90%的概率完成優化,也是非常cool的。
總結
這個問題我沒有做效能測試。其實也沒必要過於深究,與其讓編譯器以隱晦的方式完成優化,不如用程式碼進行主動、清晰的優化,讓程式碼能夠“自解釋”。
那麼,如果需要優化,使用StringBuilder吧。
本文連結:原始碼|String拼接操作”+”的優化?
作者:猴子007
出處:monkeysayhi.github.io
本文基於 知識共享署名-相同方式共享 4.0 國際許可協議釋出,歡迎轉載,演繹或用於商業目的,但是必須保留本文的署名及連結。