String拼接操作"-"的優化


String拼接操作


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

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

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

String拼接的性能問題

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

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

所謂簡單場景

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

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

<code>int i = 0;
String sentence = “Hello” + “world” + String.valueOf(i) + “\n”;
System.out.println(sentence);/<code>

利用jad可看到優化結果:

<code>int i = 0;
String sentence = (new StringBuilder()).append(“Hello”).append(“world”).append(String.valueOf(i)).append(“\n”).toString();
System.out.println(sentence);/<code>

是不是很神奇,竟然把String的拼接操作優化成了StringBuilder#append()!

此時,可以認為已經將簡單場景的空間性能、時間性能優化到最優(僅針對String拼接操作而言),看起來編譯器已經完成了必要的優化。你可以測試一下,簡單場景下的性能能夠與StringBuilder相當。但是——“但是”以前的都是廢話——編譯器的優化對於複雜場景的幫助卻很有限了。

所謂複雜場景

所謂複雜場景,可理解為“編譯器不確定(或很難確定,於是不做分析)要進行多少次字符串拼接後才需要轉換回String”。可能表述不準確,理解個大概就好。

我們分析一個最簡單的複雜場景:

<code>String sentence = “”;
for (int i = 0; i /<code>

理想的優化方案

當然,無論什麼場景,程序猿都可以手動優化:

  • 在性能敏感的場景使用StringBuilder完成拼接。
  • 在性能不敏感的場景使用更方便的String。

PS:別吐槽,這樣的API設計是合理的,在合適的地方做合適的事

理想目標是把這件事交給javac和JIT:

  • 設定一個拼接次數的閾值,超過閾值就啟動優化(對於javac有一個編譯期的閾值,JIT有一個運行期的閾值,以分階段優化)。
  • 優化時,在拼接前生成StringBuilder對象,將拼接操作換成StringBuilder#append(),繼續使用該對象,直至“需要”String對象時,使用StringBuilder#toString()“懶加載”新的String對象。

該優化方案的難度在於代碼分析:機器很難知道到底何時“需要”String對象,所以也很難在合適的位置注入代碼完成“懶加載”。

雖然很難實現,但還是給出理想的優化結果,以供實際方案對比:

<code>String sentence = “”;
StringBuilder sentenceSB = new StringBuilder(sentence);
for (int i = 0; i /<code>

實際的優化方案

利用jad查看實際的優化結果:

<code>String sentence = “”;
for (int i = 0; i /<code>

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

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

<code>String sentence = “”;
for (int i = 0; i /<code>

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

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

總結

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

那麼,如果需要優化,使用StringBuilder吧。


分享到:


相關文章: