歡迎光臨
每天分享高質量文章

Java 中字符集的編解碼

(點選上方公眾號,可快速關註)


來源:ImportNew – 郭楚沅,

我們來看看在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的編碼、解碼的時間)。

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)

測試結果

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

  • 這裡幾乎沒有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字元。

直接由 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建構式的結果,在這種情況下就要信任編碼器了。

總結

  • 首選windows-1252或者Shift_JIS這樣的本地字符集,其次才是UTF-8:(一般來說)它們生產更緊湊的二進位制資料,並且速度比編、解碼更快(在Java 7中有一些例外,但在Java 8中成為了一條規則)。

  • ISO-8859-1在Java 7和8中總是快於US-ASCII:如果你沒有充足的理由使用US-ASCII,請選擇ISO-8859-1。

  • 你可以寫一個非常快速的String->byte[]進行US-ASCII/ISO-8859-1的轉換,但是你並不能取代Java解碼器——它們直接訪問並建立String輸出。

原始碼

  • EncodingTests

    http://d1k2jhzcfaebet.cloudfront.net/wp-content/uploads/2014/04/EncodingTests.zip

看完本文有收穫?請轉發分享給更多人

關註「ImportNew」,提升Java技能

贊(0)

分享創造快樂