2016-10-26 46 views
7

我知道在最近的Java版本字符串連接Java使用+優化字符串連接多少?

String test = one + "two"+ three; 

將得到優化使用StringBuilder

但是,每次它到達這一行時,是否會生成一個新的StringBuilder,或者是否會生成一個隨後用於所有字符串連接的單個Thread Local StringBuilder?

換句話說,我可以通過創建自己的線程本地StringBuilder來重新使用還是不會有顯着的收益,從而改進經常調用的方法的性能?

我可以只寫一個測試,但我不知道它可能是編譯器/ JVM特定的或可以更普遍地回答?

+1

在連接表達式時要小心重入。 – SLaks

+1

最後我檢查它是相當愚蠢的,強制'StringBuilder'反覆重新分配。但是,這是Oracle的JDK專用的,查看得到的字節碼,因此沒有考慮JVM可能做的任何優化。我的規則是:99.999%的時間你不關心,當然;對於您關心的.001%,請使用分配足夠大的字符串以顯式處理總體結果。 –

+0

除非你正在做的更多的字符串操作,而不僅僅是那一行,我同意T.J .: 99.999%的時間你不會看到任何區別。 JVM實際上將所有內存分配爲線程本地的(直到它需要與另一個線程共享),iiuc,所以你的線程本地可能不會有任何好處。 – markspace

回答

6

據我所知,沒有編譯器生成代碼重用StringBuilder實例,最顯着javac和ECJ不生成重用代碼。

重要的是要強調,不要做這種重複使用是合理的。假設從ThreadLocal變量檢索實例的代碼比從TLAB進行普通分配的代碼更快是不安全的。即使通過試圖增加本地gc循環的潛在成本來回收該實例,就我們可以確定其在成本中的比例而言,我們也不能得出這樣的結論。

因此,試圖重用構建器的代碼會更復雜,浪費內存,因爲它會讓構建器保持活躍狀態​​,而不知道它是否會實際重用,而沒有明確的性能優勢。

尤其是當我們考慮到另外上面

  • 的JVM像熱點的聲明有逃逸分析,它可以完全的Elid純本地分配這樣的,也可能的Elid陣列的複製成本調整操作
  • 這種複雜的JVM通常還具有專門用於StringBuilder基於級聯的優化,其工作時最好編譯後的代碼如下常見圖案

隨着Java 9,圖片將再次發生變化。然後,字符串連接將被編譯爲invokedynamic指令,該指令將在運行時鏈接到JRE提供的工廠(請參閱StringConcatFactory)。然後,JRE將決定代碼的外觀,如果它對特定的JVM有好處,它可以將它定製到特定的JVM,包括緩衝區重用。這也將減少代碼的大小,因爲它只需要一條指令,而不是分配的順序和對StringBuilder的多個調用。

+1

與jdk-9的圖片變化*戲劇性*再次:) – Eugene

6

你會爲jdk-9字符串連接付出多少努力感到驚訝。第一個javac發出一個invokedynamic而不是調用StringBuilder#append。該invokedynamic將返回一個CallSite幷包含一個MethodHandle(實際上是一系列MethodHandle)。

因此,將實際完成的字符串連接的決定移至運行時。不利的一面是,你第一次連接將會變慢的字符串(對於相同類型的參數)。

然後有一系列的策略,你可以從連接字符串時選擇(可以一個通過java.lang.invoke.stringConcat參數替代默認):

private enum Strategy { 
    /** 
    * Bytecode generator, calling into {@link java.lang.StringBuilder}. 
    */ 
    BC_SB, 

    /** 
    * Bytecode generator, calling into {@link java.lang.StringBuilder}; 
    * but trying to estimate the required storage. 
    */ 
    BC_SB_SIZED, 

    /** 
    * Bytecode generator, calling into {@link java.lang.StringBuilder}; 
    * but computing the required storage exactly. 
    */ 
    BC_SB_SIZED_EXACT, 

    /** 
    * MethodHandle-based generator, that in the end calls into {@link java.lang.StringBuilder}. 
    * This strategy also tries to estimate the required storage. 
    */ 
    MH_SB_SIZED, 

    /** 
    * MethodHandle-based generator, that in the end calls into {@link java.lang.StringBuilder}. 
    * This strategy also estimate the required storage exactly. 
    */ 
    MH_SB_SIZED_EXACT, 

    /** 
    * MethodHandle-based generator, that constructs its own byte[] array from 
    * the arguments. It computes the required storage exactly. 
    */ 
    MH_INLINE_SIZED_EXACT 
} 

默認的策略是:MH_INLINE_SIZED_EXACT這是一個獸!

它採用包私有構造函數建立字符串(這是最快的):

/* 
* Package private constructor which shares value array for speed. 
*/ 
String(byte[] value, byte coder) { 
    this.value = value; 
    this.coder = coder; 
} 

首先這一戰略將創建所謂的過濾器;這些基本上都是將傳入參數轉換爲字符串值的方法句柄。正如人們所預料的,這些MethodHandles都存儲在一個名爲Stringifiers類,在大多數情況下產生MethodHandle調用:

String.valueOf(YourInstance) 

所以,如果你有,你想連接3個對象將有3個MethodHandles將委託到String.valueOf(YourObject)這實際上意味着你已經將你的對象轉換成了字符串。 這個班裏面有一些我仍然無法理解的調整,像需要分開的類StringifierMost(轉換爲僅字符串引用,浮動和雙精度)和StringifierAny

由於MH_INLINE_SIZED_EXACT表示字節數組被計算爲精確的大小;有一種計算方法。

這樣做的方式是通過StringConcatHelper#mixLen中的方法獲取輸入參數的字符串化版本(References/float/double)。此時我們知道最終字符串的大小。那麼,我們實際上並不知道它,我們有一個MethodHandle將會計算它。

String jdk-9還有一個值得一提的更改 - 增加了一個coder字段。這是計算字符串的大小/相等/字符所需的。既然它需要大小,我們也需要計算它;這是通過StringConcatHelper#mixCoder完成的。

正是在這一點上安全地委派MethodHandle,將創建烏爾數組:

@ForceInline 
    private static byte[] newArray(int length, byte coder) { 
     return (byte[]) UNSAFE.allocateUninitializedArray(byte.class, length << coder); 
    } 

如何每個元素追加?通過StringConcatHelper#prepend中的方法。

只有現在我們需要調用需要一個字節的字符串的構造函數所需的所有細節。


所有這些操作(和許多其他我都跳過爲簡單起見)通過時發出的追加實際發生,將調用的MethodHandle處理。

+1

我在這個答案中被帶走了一點,原因很簡單,這樣一個簡單的操作的細節是令人着迷的國際海事組織。 – Eugene

+0

這真的很有趣,但不幸的是它並不真的直接回答這個問題 - 所以我覺得我不能移動滴答:( –

+1

@TimB完全同意,這不是滴答:)。接受的答案是正確的。 – Eugene