2014年1月16日 星期四

String 和 StringBuilder 之間的差別

這個議題其實在 Java 當中盛行以久,應該大家都知道使用 String 相加是罪大惡極的行為
不過自從 Java 6 開始,基於各種 JVM 的調整,這個概念似乎有稍微修改的需要。

根據 [1] 的測試,作者測試了 Oracle JDK、IBM JDK 和 ECJ 三種 JVM,做出了以下的比較圖(以下三張圖皆轉錄自 [1]):




第一張圖是單純使用 String 相加,而第二、第三張圖則是使用 StringBuilder 的結果。
而圖中可以明顯看出,使用 String 相加時的效率,在第一張圖明顯低於第二、第三張圖
因此 [1] 的作者做出的結論是:沒錯,的確需要使用 StringBuilder 來提高效率!

不過跟原先大家的普遍認知不太一樣的是,實際上在 Java 6 的一些常見 JVM 實作中,String 相加都已經自動被 compiler 轉譯成 StringBuilder 了
因此使用 String 相加時,單純相加跟用 StringBuilder 是一樣的。
但在迴圈中使用 String 相加時,跟直接使用 StringBuilder 比起來仍然會有效能上的差異。
那些差異存在於,每次呼叫 String 相加時,JVM 會自動產生一個新的 StringBuilder
因此在迴圈內進行 String 相加時,雖然其實只需要一個 StringBuilder instance 就可以做完全部的字串相加
但 JVM 的轉換會讓它變成每跑一次迴圈,都會產生一次 StringBuilder,因而造成效能上的損失。

舉例來說:
String str = "";
for(..;..;..) {
  str = str + "line";
}
在 JVM 轉譯後,會變成類似這樣的實作:
String str = "";
for(..;..;..) {
  str = new StringBuilder(String.valueOf(str)).append(line).toString();
}
順便附上 [1] 原作者的結論:
  • when concatenating with plus, new instances of StringBuilder are created any time a concatenation happens. This can easily result in a performance degradation due to useless invocation of the constructor plus more stress on the garbage collector due to throw away instances
  • compilers will take you literally and only initalize StringBuilder with its 1-arg constructor if and only if you write it that way in the original code. This results in respectively four and three invocations of StringBuilder.append for CatSB and CatSB2.
關於 [2-3] 的討論,討論到 [1] 的測試並不嚴謹的問題,因為 [1] 的 test case 並沒有迴圈的情境
而實際上會造成 [1] 的效能低落是因為在運算結束後,[1] 會如同上面的程式碼那樣不斷把值 re-assign 給原先的 str 變數
re-assign 的這個動作在字串很長時一樣會造成效能瓶頸。
根據 [2] 的推文中網友 M Jwo 的回應,這種情境中正確的作法應該是這樣:
StringBuilder reportSb = new StringBuilder();
for(..;..;..){
  reportSb.append(line);
}
String report = reportSb.toString();
以上是個人的理解,如果有誤還請協助修正!
想對這個主題有更完整的認識,還是建議直接去看本文 [1-3]!

參考資料:
1、Java StringBuilder myth debunked -- now with content!
2、拆穿 Java StringBuilder 的謠言
3、Re: [翻譯] 拆穿 Java StringBuilder 的謠言

沒有留言: