调整javatm 的i/o性能
这篇文章讨论并举例阐述了提高javatm i/o性能的多种技术。绝大多数技术是围绕着磁盘文件i/o的调整来谈 的,但是,有些技术对网络i/o和视窗输出也同样适用。首先介绍的技术包含底层i/o问题,然后对诸如压 缩、格式化和序列化这样的高层i/o进行讨论。但是,请注意,本讨论不涉及应用设计问题, 搜索算法和数 据结构的选择,也不讨论类似文件高速缓存(file caching)这样的系统级问题。
当讨论java i/o时,java编程语言所假定的两种不同的磁盘文件组织是没有任何意义的。这两种磁盘文件组 织,一种基于字节流,另一种基于字符序列。在 java语言中,一个字符使用两个字节表示,而不是象c语言 那样使用一个字节表示一个字符。正因为如此,从文件中读取字符时需要一些转换。在某些情况下,这样的 区别非常重要,我们将用几个例子对此进行说明。
底层i/o问题
简介
加速i/o的基本规则
缓冲
读/写文本文件
格式化的开销
随机存储
高层i/o问题
压缩
高速缓存
标志化(tokenization)
序列化(serialization)
获取文件信息
更多的信息
加速i/o的基本规则
作为开始讨论的一种方法,下面列出了加速i/o的一些基本规则:
1.避免访问磁盘
2.避免访问下面的操作系统
3.避免方法调用
4.避免对字节和字符的单独处理
显然,这些规则不能被全面而严格地应用,因为如果那样的话,i/o就不可能工作了。但是,为了查看规则是 如何被应用的,就考虑下面的三个例子,这些例子计算一个文件中换行符(\n)的数目。
方法一:读取的方法
第一个方法简单地利用一个文件输入流(fileinputstream)上的读方法:
import java.io.*;
public class intro1 {
public static void main(string args[]) {
if (args.length != 1) {
system.err.println("missing filename");
system.exit(1);
}
try {
fileinputstream fis =
new fileinputstream(args[0]);
int cnt = 0;
int b;
while ((b = fis.read()) != -1) {
if (b == \n)
cnt++;
}
fis.close();
system.out.println(cnt);
}
catch (ioexception e) {
system.err.println(e);
}
}
}
然而,这个方法触发了大量对底层运行系统的调用,这就是fileinputstream.read, 返回文件下一个字节的本 机方法。
方法二:采用一个大缓冲区
第二种方法通过采用一个大缓冲区,避免了上述问题:
import java.io.*;
public class intro2 {
public static void main(string args[]) {
if (args.length != 1) {
system.err.println("missing filename");
system.exit(1);
}
try {
fileinputstream fis =
new fileinputstream(args[0]);
bufferedinputstream bis =
new bufferedinputstream(fis);
int cnt = 0;
int b;
while ((b = bis.read()) != -1) {
if (b == \n)
cnt++;
}
bis.close();
system.out.println(cnt);
}
catch (ioexception e) {
system.err.println(e);
}
}
}
bufferedinputstream.read从输入缓冲区中获取下一个字节,极少访问底层系统。
方法三:直接缓冲(direct buffering)
第三种方法避免使用缓冲的输入流(bufferedinputstream),而直接进行缓冲,因此避免了读取方法的调用:
import java.io.*;
public class intro3 {
public static void main(string args[]) {
if (args.length != 1) {
system.err.println("missing filename");
system.exit(1);
}
try {
fileinputstream fis =
new fileinputstream(args[0]);
byte buf[] = new byte[2048];
int cnt = 0;
int n;
while ((n = fis.read(buf)) != -1) {
for (int i = 0; i < n; i++) {
if (buf[i] == \n)
cnt++;
}
}
fis.close();
system.out.println(cnt);
}
catch (ioexception e) {
system.err.println(e);
}
}
}
对于1mb的输入文件,以秒为单位,各个程序的执行时间为:
intro1 6.9
intro2 0.9
intro3 0.4
或者,在最快和最慢之间存在一个17比1的差距。
这巨大的加速性能并没有必然地证明,应该总是效仿第三种方法,因为此方法中需要自己进行缓冲。如果事 先没有进行仔细的实现,这样的方法可能容易造成错误,特别是在处理文件结束 (end-of-file)事件时。它 也可能在可读性上比其他的方法差。但是,记住时间都花费到什么地方去了,记住在需要时如何纠正是很有 用的。
对绝大多数应用程序而言,方法二可能是正确的选择。
缓冲
方法二和方法三使用了缓冲技术,其中,文件中的一整块从磁盘中读取出来,然后再一 次一个字节或者字符 地进行访问。缓冲是加速i/o的一种基本和重要的技术,而且许多java类都支持缓冲(bufferedinputstream用于 字节,bufferedreader用于字符)。
一个明显的问题是:是否缓冲区越大就能够使i/o越快呢?java缓冲区典型的缺省值是1024或者2048个字节。 大于此值的缓冲区可能能够帮助加速i/o,但通常只有几个百分点,即5%到10%。
方法四:整个文件
这个极端的例子需要确定文件的长度,然后将整个文件读取到缓冲区中。
import java.io.*;
public class readfile {
public static void main(string args[]) {
if (args.length != 1) {
system.err.println("missing filename");
system.exit(1);
}
try {
int len = (int)(new file(args[0]).length());
fileinputstream fis =
new fileinputstream(args[0]);
byte buf[] = new byte[len];
fis.read(buf);
fis.close();
int cnt = 0;
for (int i = 0; i < len; i++) {
if (buf[i] == \n)
cnt++;
}
system.out.println(cnt);
}
catch (ioexception e) {
system.err.println(e);
}
}
}
这种方法很方便,因为文件可以被当作字节数组来对待。但是,一个明显的问题是可能没有足够的内存来读 取一个非常大的文件。
缓冲的另一方面涉及到终端窗口的文本输出。缺省情况下,system.out(一 个打印流——printstream)是行缓 冲的,也就是说,当遇到一个换行符时输出队列被清空。对于交互式应用来说,这是很重要的,阅赡芟不 对谑导适输入前,有一个输入提示符。
方法五:禁止行缓冲
但是行缓冲可以被禁止,正如下面例子中所示的:
import java.io.*;
public class bufout {
public static void main(string args[]) {
fileoutputstream fdout =
new fileoutputstream(filedescriptor.out);
bufferedoutputstream bos =
new bufferedoutputstream(fdout, 1024);
printstream ps =
new printstream(bos, false);
system.setout(ps);
final int n = 100000;
for (int i = 1; i <= n; i++)
system.out.println(i);
ps.close();
}
}
该程序输出整数1至100000,并且比使用行缓冲的程序快三倍。 缓冲也是下面几个例子之一的一个重要部分,其中缓冲区被用来加速对随机文件的访问。
读/写文本文件
先前提及的一个想法是,在从文件中读取字符时,方法调用的系统开销非常可观。这种情况的另一个例子可 以在这样一个程序中找到,该程序计算一个文本文件的行数。
import java.io.*;
public class line1 {
public static void main(string args[]) {
if (args.length != 1) {
system.err.println("missing filename");
system.exit(1);
}
try {
fileinputstream fis =
new fileinputstream(args[0]);
bufferedinputstream bis =
new bufferedinputstream(fis);
datainputstream dis =
new datainputstream(bis);
int cnt = 0;
while (dis.readline() != null)
cnt++;
dis.close();
system.out.println(cnt);
}
catch (ioexception e) {
system.err.println(e);
}
}
}
这个程序使用老版本的datainputstream.readline方法,该方法的实现是通过使用读取方法调用来获得每个字 符。一个更新的方法是:
import java.io.*;
public class line2 {
public static void main(string args[]) {
if (args.length != 1) {
system.err.println("missing filename");
system.exit(1);
}
try {
filereader fr = new filereader(args[0]);
bufferedreader br = new bufferedreader(fr);
int cnt = 0;
while (br.readline() != null)
cnt++;
br.close();
system.out.println(cnt);
}
catch (ioexception e) {
system.err.println(e);
}
}
}
这种方法能够快一些。例如,对于6mb字节200,000行的文本文件,第二个程序比第一个大约快20%。
但是,即使是第二个程序,也并不够快,这里有一个重要问题值得注意。第一个程序在javatm 2编译器上会 导致一个严厉的警告,这是因为datainputstream.readline太陈旧了,不能正确地将字节转换为字符,并且也不 是操作某些包含有非ascii字节文本文件的恰当选 择,(请注意,java语言使用unicode字符集,而不是 ascii字符集)。
前面提到的字节流和字符流的不同,在下列程序中产生了效果:
import java.io.*;
public class conv1 {
public static void main(string args[]) {
try {
fileoutputstream fos =
new fileoutputstream("out1");
printstream ps =
new printstream(fos);
ps.println("\uffff\u4321\u1234");
ps.close();
}
catch (ioexception e) {
system.err.println(e);
}
}
}
写入一个输出文件,但没有保留实际输出的unicode字符。读/写i/o类是基于字符的,并且是设计用来解决这 个问题的。在outputstreamwriter中,薪辛舜幼址蜃纸诘谋嗦胱弧?
使用printwriter输出unicode字符的程序看上去象这样:
import java.io.*;
public class conv2 {
public static void main(string args[]) {
try {
fileoutputstream fos =
new fileoutputstream("out2");
outputstreamwriter osw =
new outputstreamwriter(fos, "utf8");
printwriter pw = new printwriter(osw);
pw.println("\uffff\u4321\u1234");
pw.close();
}
catch (ioexception e) {
system.err.println(e);
}
}
}
本程序使用了utf8编码,它将ascii编码为其本身,其他字符编码为两个或者三个字节。
