Posted on 2010-01-23 21:03
周舒阳 阅读(2953)
评论(4) 编辑 收藏
本期Blog原文参见:
http://www.liferay.com/web/shuyang.zhou/blog/-/blogs/io-performance
IO操作几乎对于所有的应用都是非常重要的,因为IO操作非常容易导致性能瓶颈。
在Java的世界里存在两大类IO,传统IO(TIO)和新IO(NIO)。外加一个即将到来的增强版的NIO——NIO2(JDK7)。
NIO(以及NIO2)主要用于在一些特定情况下增强性能、提供更好的操作系统层次IO功能集成,但它们无法完全替代TIO!在许多情况下TIO仍然是你唯一的选择。
今天我们就来讨论一下TIO的性能问题。
IO的性能瓶颈主要分为两类:
- 错误的使用缓冲(buffer)
- 过度的同步保护
我们都知道buffer能够增加IO的性能,但不是每个人都知道如何正确的使用buffer,在本文结束时我会给出一些最佳实践建议。
第一部分:
对于1,错误的使用缓冲(buffer),存在两点非常流行的错误用法和一个感念上的误解
- a)为内存IO类(In-memory IO class)添加缓冲(错误用法)
- b)为已添加buffer的IO类再次添加buffer(错误用法)
- c)Buffer版的IO类和显式使用buffer间的关系(概念上的误解)
对于a),这是非常荒谬的!添加buffer的目的是为了将对IO设备的多次访问合并为一次访问从而提高性能,In-memory类(如ByteArrayInput/OutputStream)根本就不会访问任何IO设备,所以对它们添加buffer是完全没有必要的。
对于b),这是完全多余的!你只需要buffer一次就可以了,多于一次的buffer只会引入更多的栈调用和垃圾创建。
对于c),这需要多一点解释:
从本质上讲,这两种做法是要达到相同的目的,但方法不同,这也导致了它们之间具有巨大的性能差异!
针对这一问题我做了一个测试,比较使用Buffer版的IO类和显式使用buffer在读/写文件时的性能差异。
下面的测试结果显式了两者的性能差异:
读测试结果:(所有数据都是在JVM预热后取得的,每个采样点的时间是10次读取操作的总时间,单位为毫秒)
文件大小 |
1K |
10K
|
100K
|
1M
|
10M
|
100M
|
1G
|
BufferedInputStream |
0 |
1
|
5 |
53 |
549 |
5492 |
56002 |
显式使用byte[]在FileInputStream上读取 |
0 |
0 |
1
|
10
|
113
|
1126
|
11448
|
写测试结果:(所有数据都是在JVM预热后取得的,每个采样点的时间是10次写入操作的总时间,单位为毫秒)
文件大小 |
1K |
10K
|
100K
|
1M
|
10M
|
100M
|
1G
|
BufferedOutputStream |
0 |
1
|
5 |
45 |
472 |
4793 |
48794 |
显式使用byte[]在FileOuputStream上写入 |
0 |
1 |
1
|
10
|
124
|
1300
|
13138
|
为什么会有如此大的性能差异呢?有两点原因:
-
Buffer版的IO类导致很多无谓的栈调用(都是装饰者模式惹得祸decorator pattern)
-
JDK中所有的Buffer版IO类都是线程安全的,这就意味着它们添加了大量的同步保护(将在第二部分中详细解释)
现在你知道了显式使用buffer要比使用Buffer版的IO类具有更好的性能,所以请尽量多显式使用buffer,但在两种特殊情况下你仍然需要Buffer版的IO类:
-
当你在使用第三方库时,库的api需要IO类作为参数,并且你确定他们内部的代码采用流式方式编码(非块式操作),也就是没有显式使用buffer。在不修改他们代码的前提下,你只能通过传入Buffer版的IO类对象来提升性能。
-
另外一种情况就是,如果你很懒,你会更倾向于使用Buffer版的IO类,因为它们能节省你几行代码(相对于显式使用buffer)。
第二部分:
对于2,过度的同步保护,我指的是JDK的IO包,也就是java.io包。
我一点也不喜欢这个包里面的代码,因为他们都是线程安全的,也就意味着许多同步保护。如果我需要同步保护,我会自己去做,而且我绝不会去添加任何多余的保护。但在这一点上JDK的IO包把我逼得无路可走
只要你使用JDK的IO包,你就被迫的添加了许多同步保护,即使你完全确定你的代码运行在单一线程的环境下,你也不能回避这些不必要的保护。你也许会好奇的问,这真的是一个严重的问题吗?JVM在运行时会对弱竞争的锁进行优化,不是吗?显然,它做的优化还不到家,让我们来看一下性能测试结果。
我翻版了一批JDK IO包中常用的类,这个翻版完全是API层次的翻版,也就是说全部代码是参照JDK IO包的Javadoc写成, 没有直接借用JDK的源码。因为JDK源码大部分使用GPL协议发布,而Liferay的代码采用MIT协议发布,为了不引起IP纠纷只好照葫芦画瓢。而实际上从0开始创建这些类一点也不难(仅仅是装饰者模式而已),只是非常的繁琐。在我的翻版类中,我移除了全部的同步保护。而我的测试也进行在单一线程环境下,所以不用担心线程安全的问题。
测试包含两部分,第一部分比较原始JDK IO类和我的unsyc版的IO类在读取内存数据(In-memory data)时的性能差异,第二部分比较原始JDK IO类和我的unsyc版的IO类在写入内存数据(In-memory data)时的性能差异。之所以采用内存数据而不是磁盘数据是为了放大同步操作对整体性能的影响,以便于分析。
读测试结果:
写测试结果:
写数据的测试曲线不像读的那样平滑,原因在于它内部使用了一个动态增长的byte[],这导致大量GC活动(与上一期Blog中我们讨论SB时看到的问题相似)。
好了,现在你应该看到了同步保护是一项多么沉重的操作。我们日常开发中存在大量局限在方法调用栈内的IO类使用,这些情况都是绝对发生在单一线程环境下的。另外一些时候,即使IO对象的引用超出了方法调用栈的作用域,但我们可以通过分析得知它仍然只会被单一线程所访问,比如web开发中,针对一个request的全部处理一般都是由一个worker thread来完成的(除非你的后台还有其他的异步服务线程与worker间交换数据,但这很少见)。对于这样的情况,你大可以放心的使用这些unsyc的IO类(com.liferay.portal.kernel.io.unsync)。
更多关于unsycIO类的信息请查看Liferay的JIRA链接:
http://issues.liferay.com/browse/LPS-6648
最后给大家留下一些建议:
-
如果可能请尽量显式使用buffer,而不是使用Buffer版的IO类。
-
仅在使用第三方类库和你很懒的时候使用Buffer版的IO类。
-
当你确定你的代码运行在单一线程环境下,或者你自己添加了同步保护时,请使用com.liferay.portal.kernel.io.unsync包中的IO类。它们能大幅提高你的应用的IO性能。
这里我提供了一个消除了对Liferay其他类文件依赖的com.liferay.portal.kernel.io.unsync包供大家下载使用。不过还是推荐大家直接学习使用Liferay:)
http://www.blogjava.net/Files/ShuyangZhou/IOPerformance/src.zip