Java 7/8中字符集的编解码

jopen 10年前

我们来看看在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

 

原文链接: java-performance 翻译: ImportNew.com - Grey
译文链接: http://www.importnew.com/13331.html