源碼|String拼接操做」+」的優化?

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

須要關注的是編譯器對String拼接作出的優化,在簡單場景下的性能可以與StringBuilder至關,複雜場景下仍然有較大的性能問題。網上關於這一問題講的很是亂;若是我講的有什麼紕漏,也歡迎指正。git

JDK版本:oracle java 1.8.0_102github

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

String拼接的性能問題

優化以前,每次用」+」拼接,都會生成一個新的String。特別在循環拼接字符串的場景下,性能損失是極其嚴重的:oracle

  1. 空間浪費:每次拼接的結果都須要建立新的不可變類
  2. 時間浪費:建立的新不可變類須要初始化;產生大量「短命」垃圾,影響 young gc甚至full gc

所謂簡單場景

簡單場景和複雜場景是我亂起的名字,幫助理解編譯器的優化方案。app

簡單場景可理解爲在一句中完成拼接:工具

int i = 0;
String sentence = 「Hello」 + 「world」 + String.valueOf(i) + 「\n」;
System.out.println(sentence);複製代碼

利用jad可看到優化結果:性能

int i = 0;
String sentence = (new StringBuilder()).append(「Hello」).append(「world」).append(String.valueOf(i)).append(「\n」).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) + 「\n」;
}
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(「\n」);
}
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(「\n」).toString();
}
System.out.println(sentence);複製代碼

能夠看到,實際上編譯器的優化只能達到簡單場景的最優:僅優化字符串拼接的一句。這種優化程度,對於上述複雜場景的性能提高頗有限,循環時仍是會生成大量短命垃圾,特別是字符串拼接到很大的時候,空間和時間上都是致命的。

經過對理想方案的分析,咱們也能理解編譯器優化的無奈之處:編譯器沒法(或很難)經過代碼分析判斷什麼時候是最晚進行懶加載的時機。爲何呢?咱們將代碼換個形式可能更容易理解:

String sentence = 「」;
for (int i = 0; i < 10000000; i++) {
  sentence = sentence + 「Hello」 + 「world」 + String.valueOf(i) + 「\n」;
}
System.out.println(sentence);複製代碼

觀察第3行的代碼,等式右側引用了sentence。我肉眼知道這句話只完成了字符串拼接,機器呢?最起碼,如今的機器還很難經過代碼判斷。

待之後將人工智能與編譯優化結合起來,就算只能以90%的機率完成優化,也是很是cool的。

總結

這個問題我沒有作性能測試。其實也不必過於深究,與其讓編譯器以隱晦的方式完成優化,不如用代碼進行主動、清晰的優化,讓代碼可以「自解釋」。

那麼,若是須要優化,使用StringBuilder吧。


本文連接:源碼|String拼接操做」+」的優化?
做者:猴子007
出處:monkeysayhi.github.io
本文基於 知識共享署名-相同方式共享 4.0 國際許可協議發佈,歡迎轉載,演繹或用於商業目的,可是必須保留本文的署名及連接。

相關文章
相關標籤/搜索