Java 7/8中字符集的編解碼

jopen 10年前發布 | 12K 次閱讀 Java Java開發

我們來看看在Java 7/8中字符集編碼和解碼的性能。先看看下面兩個String方法在不同字符集下的性能:

/* String to byte[] */
public byte[] getBytes(Charset charset);
/* byte[] to String */
public String(byte bytes[], Charset charset);

我把“Develop with pleasure”通過谷歌翻譯為德語、俄語、日語和繁體中文。我們將根據這些短語構建指定大小的塊,通過使用“\n”作為分隔符來連接它們直到到達指定的長度(在大多數情況下,結果會稍長一些)。在那之后我們將100M字符的byte[]數據轉化為String數據(100M是Java中char字符的總長度)。我們將轉換10遍以確保結果更加可靠(因此,在下表中是轉換10億字符的時間)。

我們將使用2個塊的大小:100個字符用于測試短字符串轉換的性能,100M字符用來測試最初的轉換性能,你可以在本文末尾找到文章的源代碼。我們會用UTF-8的方式與“本地化的”字符集進行比較(英語US-ASCII、德語ISO-8859-1、俄語windows-1251、日語Shift_JIS、繁體中文GB18030),將UTF-8作為通用編碼時這些信息會非常很有(通常意味著更大的二進制轉換開銷)。我們也會對比Java 7u51和Java 8(的版本特性)。為了避免GC帶來的影響,所有測試都是在我搭載Xmx32G的Xeon-2650(2.8Ghz)工作站上運行。

以下是測試結果。每個實例有兩個時間結果:Java7的時間(和Java8的時間)。”UTF-8″這一行遵循了每個“本地化的”字符集,它包含從前一行數據的轉換時間(例如,最后一行包括了string從繁體中文轉為UTF-8的編碼、解碼的時間)。

</tr>

</tr>

</tr>

</tr>

</tr>

</tr>

</tr>

</tr>

</tr>

</tr>

</tr> </tbody> </table>

測試結果

我們可以注意到以下事實:

  • 這里幾乎沒有CPU開銷的分塊輸出——如果你為這個測試分配更少的內存,那么分塊結果將變得更糟。
  • 如果是單字節字符集,那么將byte[]轉換為String將非常快(US-ASCII、ISO-8859-1和windows-1251):一旦知道輸入數據的大小,那么就可以分配結果中char[]的合適大小。同時,如果是在java.lang包中,可以使用一個受保護的String構造函數,這并不需要char[]的拷貝。
  • 同時,String.getBytes(UTF-8)對于non-ASCII編碼不能高效地工作——包括更復雜的映射,它分配了最大可能的char[]輸出,然后復制實際使用的部分給String的返回結果。UTF-8轉換中文/日文的速度確實非常慢。
  • 如果是“本地化的”字符集,String -> byte[]的轉換效率通常是低于byte[] -> String的。出人意料的是,在使用UTF-8時會觀察到相反的結果:String -> byte[]普遍快于byte[] -> String
  • Shift_JIS和ISO-8859-1的轉換(可能也包括一些其它字符集)在Java 8中進行了極大的優化(綠色高亮):相比Java 7,Java8對日語轉換的速度要快2-3倍。在ISO-8859-1的情況下,只有String -> byte[]進行了優化——它的運行速度比現在要快七倍!這個結果聽起來確實令我吃驚(請接著往下看)。
  • 一個更加明顯的區別是:byte[] -> String對于windows-1251與UTF-8編碼轉換時間的比較(紅色高亮)。它們大約相差六倍(windows-1251比UTF-8快六倍)。我不確定是否有可能證明它只是由不同的二進制表示:如果使用windows-1251,每個字符你需要1個字節的消耗;而如果使用UTF-8,對于俄語字符集則是每個字符兩個字節。ISO-8859-1和UTF-8之間是有大同小異的地方的(藍色高亮): 在德語字符串中只有一個字符不需要用2個UTF-8字符表示。而在俄語字符串中,(除空格外)幾乎每個字符都需要2個UTF-8字符。
  • </ul>

    直接由 String->byte[]->String 轉換為 ASCII / ISO-8859-1 數據

    我嘗試過研究Java 8中的ISO-8859-1編碼器的表現。其算法本身非常簡單,ISO-8859-1字符集完全匹配Unicode表中前255個字符的位置,所以看起來像下面這樣:

    if ( char <= 255 )
        write it as byte to output
    else
        skip input char, write Charset.replacement byte

    Java 7 和 8中ISO_8859_1.java的不同之處,Java 7在單一方法中包含了各種優先權編碼邏輯,但是Java 8提供了幫助方法(Helper Method)。當沒有字符大于255時,將輸入的char[]進行轉換。我認為這種方法使得JIT產生更多高效的代碼。

    眾所周知,US-ASCII或者ISO-8859-1的編碼器優于JDK編碼器。只需要假設字符串僅包含有效的字符編碼并且避免所有的“管道(plumbing)”:

    private static byte[] toAsciiBytes( final String str )
    {
        final byte[] res = new byte[ str.length() ];
        for (int i = 0; i < str.length(); i++)
            res[ i ] = (byte) str.charAt( i );
        return res;
    }

    這種方式取代了Java 8中20-25%的ISO-8859-1編碼器,同時效率是Java 7的3到3.5倍。然而,它依賴JIT來進行數據訪問和String.charAt的邊界檢查。

    對于這兩個數據集,取代byte[] -> String轉換幾乎是不可能的。因為沒有公共的String構造函數或工廠方法,這將使用你提供的char[]類型。它們都進行了保護性的備份(否則將無法保證String的不變性)。性能方面最接近的是一個被棄用的String(byte ascii[], int hibyte, int offset, int count)構造函數。如果你的字符集匹配的是一個255字節的Unicode(US-ASCII, ISO-8859-1),那么對于byte[]->String編碼器而言是非常有用的。不幸的是,這個構造函數從字符串結尾開始復制數據,并不像CPU緩存那么友好。

    private static String asciiBytesToString( final byte[] ascii )
    {
        //deprecated constructor allowing data to be copied directly into String char[]. So convenient...
        return new String( ascii, 0 );
    }

    另一方面,String(byte bytes[],int offset, int length, Charset charset)減少了所有可能的邊界類型(edge):對于US-ASCII和ISO-8859-1,它分配了char[]所需的大小,進行一次低成本轉換(使byte變為 char)同時提供char[]轉為String構造函數的結果,在這種情況下就要信任編碼器了。

    總結

Charset getBytes, ~100 chars (chunk size) new String, ~100 chars (chunk size) getBytes, ~100M chars new String, ~100M chars
US-ASCII 2.451 sec(2.686 sec) 0.981 sec(0.971 sec) 2.402 sec(2.421 sec) 0.889 sec(0.903 sec)
UTF-8 1.193 sec(1.259 sec) 0.974 sec(1.181 sec) 1.226 sec(1.245 sec) 0.887 sec(1.09 sec)
ISO-8859-1 2.42 sec(0.334 sec) 0.816 sec(0.84 sec) 2.441 sec(0.355 sec) 0.761 sec(0.801 sec)
UTF-8 3.14 sec(3.534 sec) 3.373 sec(4.134 sec) 3.288 sec(3.498 sec) 3.314 sec(4.185 sec)
windows-1251 5.85 sec(5.826 sec) 2.004 sec(1.909 sec) 5.881 sec(5.747 sec) 1.902 sec(1.87 sec)
UTF-8 5.425 sec(5.256 sec) 11.561 sec(12.326 sec) 5.544 sec(4.921 sec) 11.29 sec(12.314 sec)
Shift_JIS 17.343 sec(9.355 sec) 24.85 sec(8.464 sec) 16.95 sec(9.24 sec) 24.6 sec(8.503 sec)
UTF-8 9.398 sec(13.201 sec) 12.007 sec(16.661 sec) 9.681 sec(11.801 sec) 12.035 sec(16.602 sec)
GB18030 18.754 sec(16.641 sec) 15.877 sec(16.267 sec) 18.494 sec(16.342 sec) 16.034 sec(16.406 sec)
UTF-8 9.374 sec(11.829 sec) 12.092 sec(16.672 sec) 9.678 sec(12.991 sec) 12.25 sec(16.745 sec)
  • sesese色