内容目录
1.1 I/O与CPU时间的比较
程序员多半当自个儿是软件大师,设计出精巧的例程,这儿压缩几个字节,那儿解开一个循环,要不就在别处作些调整,让对象更加牢固。这些事情当然很重要,乐趣也不少,但是代码优化所带来的回报,可能轻易就被低效的 I/O 所抵销。I/O 操作比在内存中进行数据处理任务所需时间更长,差别要以数量级计。许多程序员一门心思扑在他们的对象如何加工数据上,对影响数据取得和存储的环境问题却不屑一顾。
表 1-1 所示为对数据单元进行磁盘读写所需时间的假设值。第一列为处理一个数据单元所需平均时间,第二列为对该数据单元进行磁盘读写所需时间,第三列为每秒所能处理的数据单元数,第四列为改变第一第二列的值所能产生的数据吞吐率的提升值。
表 1-1. 处理时间与 I/O 时间对吞吐率的影响比较 | |||
---|---|---|---|
处理时间(ms) | I/O 时间(ms) | 吞吐率(units/sec) | 增益(%) |
5 | 100 | 1000/(5+100)=9.52 | (基准) |
2.5 | 100 | 1000/(2.5+100)=9.76 | 2.44 |
1 | 100 | 1000/(1+100)=9.9 | 3.96 |
5 | 90 | 1000/(5+90)=10.53 | 10.53 |
5 | 75 | 1000/(5+75)=12.5 | 31.25 |
5 | 50 | 1000/(5+50)=18.18 | 90.91 |
5 | 20 | 1000/(5+20)=40 | 320 |
5 | 10 | 1000/(5+10)=66.67 | 600 |
前三行显示了处理阶段的效率提升会如何影响吞吐率。把单位处理时间减半,仅能提高吞吐率2.2%。而另一方面,仅仅缩短 I/O 延迟 10%,就可使吞吐率增加9.7%;把 I/O 时间减半,吞吐率几乎翻番。当您了解到 I/O 花在一个数据单元上的时间是处理时间的 20 倍,这样的结果就不足为奇了。
表中所列并非真实数据,目的只在说明相对时间度量,现实情况绝非如此简单。正如您所看到的,影响应用程序执行效率的限定性因素,往往并非处理速率,而是 I/O。程序员热衷于调试代码,I/O 性能的调试往往被摆在第二位,甚至完全忽略。殊不知,在 I/O 性能上的小小投入就可换来可观的回报,想来实在令人惋惜。
1.2 CPU已不再是束缚
Java程序员把全部精力用在优化处理效率上,而对 I/O 关注不足,在某种程度上讲这并非他们的错。在 Java 的早期,JVM 在解释字节码时往往很少或没有运行时优化。这就意味着,Java 程序往往拖得很长,其运行速率大大低于本地编译代码,因而对操作系统 I/O 子系统的要求并不太高。如今在运行时优化方面,JVM 已然前进了一大步。现在 JVM 运行字节码的速率已经接近本地编译代码,借助动态运行时优化,其表现甚至还有所超越。这就意味着,多数 Java 应用程序已不再受 CPU 的束缚(把大量时间用在执行代码上),而更多时候是受 I/O 的束缚(等待数据传输)。
然而,在大多数情况下,Java 应用程序并非真的受着 I/O 的束缚。操作系统并非不能快速传送数据,让 Java 有事可做;相反,是 JVM 自身在 I/O 方面效率欠佳。操作系统与 Java 基于流的 I/O模型有些不匹配。操作系统要移动的是大块数据(缓冲区),这往往是在硬件直接存储器存取(DMA)的协助下完成的。而 JVM 的 I/O 操作类喜欢操作小块数据——单个字节、几行文本。结果,操作系统送来整缓冲区的数据,java.io 包的流数据类再花大量时间把它们拆成小块,往往拷贝一个小块就要往返于几层对象。操作系统喜欢整卡车地运来数据,java.io 类则喜欢一铲子一铲子地加工数据。有了 NIO,就可以轻松地把一卡车数据备份到您能直接使用的地方(ByteBuffer 对象)。
这并不是说使用传统的 I/O 模型无法移动大量数据——当然可以(现在依然可以)。具体地说,RandomAccessFile
类在这方面的效率就不低,只要坚持使用基于数组的read()
和write()
方法。
这些方法与底层操作系统调用相当接近,尽管必须保留至少一份缓冲区拷贝。如表 1-1 所示,如果您的代码大部分时间都处于 I/O 等待状态,那么,该考虑一下提升 I/O 效率的问题了,否则,您精心打造的代码多数时间都得闲着。
内容来源于《java nio》,略有改动。
0 条评论
撰写评论