调整 JavaTM I/O 性能
Glen McCluskey
翻译cherami
这篇文章讨论和阐明了提供 JavaTM I/O 性能的多种技术大多技术围绕调整磁盘文件 I/O但是有些内容也同样适合网络 I/O 和窗口输出第一部分技术讨论底层的I/O问题然后讨论诸如压缩格式化和串行化等高级I/O问题然而这个讨论没有包含应用设计问题例如搜索算法和数据结构也没有讨论系统级的问题例如文件高速缓沖
当我们讨论Java I/O时值得注意的是Java语言采取两种截然不同的磁盘文件结构一个是基于字节流另一个是字符序列在Java语言中一个字符有两个字节表示而不是像通常的语言如c语言那样是一个字节因此从一个文件读取字符时需要进行转换这个不同在某些情况下是很重要的就像下面的几个例子将要展示的那样
低级 I/O 问题
加速I/O的基本规则
缓沖
读写文本文件
格式化的代价
随机访问
高级I/O问题
压缩
高速缓沖
分解
串行化
获取文件信息
更多信息
加速I/O的基本规则
作为这个讨论的开始这里有几个如何加速I/O的基本规则:
避免访问磁盘
避免访问底层的操作系统
避免方法调用
避免个别的处理字节和字符
很明显这些规则不能在所有的问题上避免因为如果能够的话就没有实际的I/O被执行考虑下面的计算文件中的新行符(\n)的三部分范例
方法: read方法
第一个方法简单的使用FileInputStream的read方法:
import javaio*;
public class intro {
public static void main(String args[]) {
if (argslength != ) {
Systemerrprintln(missing filename);
Systemexit();
}
try {
FileInputStream fis =
new FileInputStream(args[]);
int cnt = ;
int b;
while ((b = fisread()) != ) {
if (b == \n)
cnt++;
}
fisclose();
Systemoutprintln(cnt);
}
catch (IOException e) {
Systemerrprintln(e);
}
}
}
然而这个方法触发了大量的底层运行时系统调用--FileInputStreamread--返回文件的下一个字节的本机方法
方法 : 使用大缓沖区
第二种方法使用大缓沖区避免了上面的问题:
import javaio*;
public class intro {
public static void main(String args[]) {
if (argslength != ) {
Systemerrprintln(missing filename);
Systemexit();
}
try {
FileInputStream fis =
new FileInputStream(args[]);
BufferedInputStream bis =
new BufferedInputStream(fis);
int cnt = ;
int b;
while ((b = bisread()) != ) {
if (b == \n)
cnt++;
}
bisclose();
Systemoutprintln(cnt);
}
catch (IOException e) {
Systemerrprintln(e);
}
}
}
BufferedInputStreamread 从输入缓沖区获取下一个字节仅仅只访问了一次底层系统
方法 : 直接缓沖
第三种方法避免使用 BufferedInputStream 而直接缓沖因此排除了 read 方法的调用:
import javaio*;
public class intro {
public static void main(String args[]) {
if (argslength != ) {
Systemerrprintln(missing filename);
Systemexit();
}
try {
FileInputStream fis =
new FileInputStream(args[]);
byte buf[] = new byte[];
int cnt = ;
int n;
while ((n = fisread(buf)) != ) {
for (int i = ; i < n; i++) {
if (buf[i] == '\n')
cnt++;
}
}
fis.close();
System.out.println(cnt);
}
catch (IOException e) {
System.err.println(e);
}
}
}
对于一个1 MB 的输入文件,以秒为单位的执行时间是:
intro1 6.9
intro2 0.9
intro3 0.4
或者说在最慢的方法和最快的方法间是17比1的不同。tw.wINgwIT.cOm
这个巨大的加速并不能证明你应该总是使用第三种方法,即自己做缓沖。这可能是一个错误的倾向特别是在处理文件结束事件时没有仔细的实现。在可读性上它也没有其它方法好。但是记住时间花费在哪儿了以及在必要的时候如何矫正是很有用。
方法2 或许是对于大多应用的 "正确" 方法.
缓沖
方法 2 和 3 使用了缓沖技术, 大块文件被从磁盘读取,然后每次访问一个字节或字符。缓沖是一个基本而重要的加速I/O 的技术,而且有几个类支持缓沖(BufferedInputStream 用于字节, BufferedReader 用于字符)。
一个明显得问题是: 缓沖区越大I/O越快吗?典型的Java缓沖区长1024 或者 2048 字节,一个更大的缓沖区有可能加速 I/O但是只能占很小的比重,大约5 到10%。
方法4: 整个文件
缓沖的极端情况是事先决定整个文件的长度,然后读取整个文件:
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) 是行缓沖的,这意味着在遇到一个新行符后输出缓沖区被提交。对于交互来说这是很重要的,在那种情况下你可能喜欢在实际的输出前显示一个输入提示。
方法 5: 关闭行缓沖
行缓沖可以被禁止,像下面的例子那样:
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();
}