原始碼|String拼接操作”+”的優化?

monkeysayhi發表於2019-03-04

很多講Java優化的文章都會強調對String拼接的優化。倒不用特意記,本質上在於對不可變類優勢和劣勢的理解上。

需要關注的是編譯器對String拼接做出的優化,在簡單場景下的效能能夠與StringBuilder相當,複雜場景下仍然有較大的效能問題。網上關於這一問題講的非常亂;如果我講的有什麼紕漏,也歡迎指正。

JDK版本:oracle java 1.8.0_102

本文用到了反編譯工具jad。在查閱網上關於String拼接操作的優化時發現了這個工具,能同時反編譯出來原始碼和位元組碼,親測好用,點我下載

String拼接的效能問題

優化之前,每次用”+”拼接,都會生成一個新的String。特別在迴圈拼接字串的場景下,效能損失是極其嚴重的:

  1. 空間浪費:每次拼接的結果都需要建立新的不可變類
  2. 時間浪費:建立的新不可變類需要初始化;產生大量“短命”垃圾,影響 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 國際許可協議釋出,歡迎轉載,演繹或用於商業目的,但是必須保留本文的署名及連結。

相關文章