(點選上方公眾號,可快速關註)
來源: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技能