Java 7/8中字符集的编解码
我们来看看在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
输出。
源代码
原文链接: java-performance 翻译: ImportNew.com - Grey
译文链接: http://www.importnew.com/13331.html