很多講Java優(yōu)化的文章都會強(qiáng)調(diào)對String拼接的優(yōu)化。倒不用特意記陈瘦,本質(zhì)上在于對不可變類優(yōu)勢和劣勢的理解上幌甘。
需要關(guān)注的是編譯器對String拼接做出的優(yōu)化,在簡單場景下的性能能夠與StringBuilder相當(dāng),復(fù)雜場景下仍然有較大的性能問題锅风。網(wǎng)上關(guān)于這一問題講的非常亂酥诽;如果我講的有什么紕漏,也歡迎指正皱埠。
JDK版本:oracle java 1.8.0_102
本文用到了反編譯工具jad盆均。在查閱網(wǎng)上關(guān)于String拼接操作的優(yōu)化時發(fā)現(xiàn)了這個工具,能同時反編譯出來源碼和字節(jié)碼漱逸,親測好用泪姨,點我下載。
String拼接的性能問題
優(yōu)化之前饰抒,每次用”+”拼接肮砾,都會生成一個新的String。特別在循環(huán)拼接字符串的場景下袋坑,性能損失是極其嚴(yán)重的:
- 空間浪費(fèi):每次拼接的結(jié)果都需要創(chuàng)建新的不可變類
- 時間浪費(fèi):創(chuàng)建的新不可變類需要初始化仗处;產(chǎn)生大量“短命”垃圾,影響 young gc甚至full gc
所謂簡單場景
簡單場景和復(fù)雜場景是我亂起的名字枣宫,幫助理解編譯器的優(yōu)化方案婆誓。
簡單場景可理解為在一句中完成拼接:
int i = 0;
String sentence = “Hello” + “world” + String.valueOf(i) + “\n”;
System.out.println(sentence);
利用jad可看到優(yōu)化結(jié)果:
int i = 0;
String sentence = (new StringBuilder()).append(“Hello”).append(“world”).append(String.valueOf(i)).append(“\n”).toString();
System.out.println(sentence);
是不是很神奇,竟然把String的拼接操作優(yōu)化成了StringBuilder#append()也颤!
此時洋幻,可以認(rèn)為已經(jīng)將簡單場景的空間性能、時間性能優(yōu)化到最優(yōu)(僅針對String拼接操作而言)翅娶,看起來編譯器已經(jīng)完成了必要的優(yōu)化文留。你可以測試一下,簡單場景下的性能能夠與StringBuilder相當(dāng)竭沫。但是——“但是”以前的都是廢話——編譯器的優(yōu)化對于復(fù)雜場景的幫助卻很有限了燥翅。
所謂復(fù)雜場景
所謂復(fù)雜場景,可理解為“編譯器不確定(或很難確定蜕提,于是不做分析)要進(jìn)行多少次字符串拼接后才需要轉(zhuǎn)換回String”森书。可能表述不準(zhǔn)確谎势,理解個大概就好凛膏。
我們分析一個最簡單的復(fù)雜場景:
String sentence = “”;
for (int i = 0; i < 10000000; i++) {
sentence += “Hello” + “world” + String.valueOf(i) + “\n”;
}
System.out.println(sentence);
理想的優(yōu)化方案
當(dāng)然,無論什么場景它浅,程序猿都可以手動優(yōu)化:
- 在性能敏感的場景使用StringBuilder完成拼接译柏。
- 在性能不敏感的場景使用更方便的String镣煮。
PS:別吐槽姐霍,這樣的API設(shè)計是合理的,在合適的地方做合適的事。
理想目標(biāo)是把這件事交給javac和JIT:
- 設(shè)定一個拼接次數(shù)的閾值镊折,超過閾值就啟動優(yōu)化(對于javac有一個編譯期的閾值胯府,JIT有一個運(yùn)行期的閾值,以分階段優(yōu)化)恨胚。
- 優(yōu)化時骂因,在拼接前生成StringBuilder對象,將拼接操作換成StringBuilder#append()赃泡,繼續(xù)使用該對象寒波,直至“需要”String對象時,使用StringBuilder#toString()“懶加載”新的String對象升熊。
該優(yōu)化方案的難度在于代碼分析:機(jī)器很難知道到底何時“需要”String對象俄烁,所以也很難在合適的位置注入代碼完成“懶加載”。
雖然很難實現(xiàn)级野,但還是給出理想的優(yōu)化結(jié)果页屠,以供實際方案對比:
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);
實際的優(yōu)化方案
利用jad查看實際的優(yōu)化結(jié)果:
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);
可以看到,實際上編譯器的優(yōu)化只能達(dá)到簡單場景的最優(yōu):僅優(yōu)化字符串拼接的一句蓖柔。這種優(yōu)化程度辰企,對于上述復(fù)雜場景的性能提升很有限,循環(huán)時還是會生成大量短命垃圾况鸣,特別是字符串拼接到很大的時候牢贸,空間和時間上都是致命的。
通過對理想方案的分析镐捧,我們也能理解編譯器優(yōu)化的無奈之處:編譯器無法(或很難)通過代碼分析判斷何時是最晚進(jìn)行懶加載的時機(jī)十减。為什么呢?我們將代碼換個形式可能更容易理解:
String sentence = “”;
for (int i = 0; i < 10000000; i++) {
sentence = sentence + “Hello” + “world” + String.valueOf(i) + “\n”;
}
System.out.println(sentence);
觀察第3行的代碼愤估,等式右側(cè)引用了sentence帮辟。我肉眼知道這句話只完成了字符串拼接,機(jī)器呢玩焰?最起碼由驹,現(xiàn)在的機(jī)器還很難通過代碼判斷。
待以后將人工智能與編譯優(yōu)化結(jié)合起來昔园,就算只能以90%的概率完成優(yōu)化蔓榄,也是非常cool的。
總結(jié)
這個問題我沒有做性能測試默刚。其實也沒必要過于深究甥郑,與其讓編譯器以隱晦的方式完成優(yōu)化,不如用代碼進(jìn)行主動荤西、清晰的優(yōu)化澜搅,讓代碼能夠“自解釋”伍俘。
那么,如果需要優(yōu)化勉躺,使用StringBuilder吧癌瘾。
本文鏈接:源碼|String拼接操作”+”的優(yōu)化?
作者:猴子007
出處:https://monkeysayhi.github.io
本文基于 知識共享署名-相同方式共享 4.0 國際許可協(xié)議發(fā)布饵溅,歡迎轉(zhuǎn)載妨退,演繹或用于商業(yè)目的,但是必須保留本文的署名及鏈接蜕企。