M12i.

学術書・マンガ・アニメ・映画の消費活動とプログラミングについて

BufferedInputStreamの威力を実験してみる

ずいぶん以前の記事になるけれどJavaのパフォーマンス最適化に関する論文を読んでいたところ、BufferedInputStream(およびBufferedOutputStream)を使用することでI/Oのパフォーマンスが非常に良くなるという記述があったので実際に試してみた。

使用しているJVMは以下のとおり:

$ java -version
java version "1.7.0_55"
Java(TM) SE Runtime Environment (build 1.7.0_55-b13)
Java HotSpot(TM) 64-Bit Server VM (build 24.55-b03, mixed mode)

実験プログラム

実験用プログラムは非常に単純で、以下の手順を3回繰り返す:

  1. "あいうえお…"の羅列で300MB強のファイルを作成
  2. FileInputStreamをBufferedInputStreamでラップせず直接InputStreamReaderでラップしてファイル内容をすべて読み込む。
  3. FileInputStreamをBufferedInputStreamでラップしてからInputStreamReaderでラップしてファイル内容をすべて読み込む。

人に見せるようなシロモノでもないけれどソースコードは以下のとおり:

package buffered.reader.study;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.charset.Charset;

public class Main {

	private static final String filePath = "aiueo.txt";
	private static String msg = "";
	private static long msec = System.currentTimeMillis();
	
	public static void main(String[] args) throws IOException {
		run();
		run();
		run();
	}
	
	private static void run() throws IOException {
		saveMsec("入力ファイルの作成");
		final FileOutputStream fos = new FileOutputStream(filePath);
		final BufferedOutputStream bos = new BufferedOutputStream(fos);
		final OutputStreamWriter osw = new OutputStreamWriter(bos, Charset.defaultCharset());
		final PrintWriter pr = new PrintWriter(osw);
		for (int i = 0; i < 1000000; i++) {
			// あいうえお * 20
			pr.println("あいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえお"
					+ "あいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえお");
		}
		pr.close();
		printMsecDelta();
		
		saveMsec("FileInputStreamを直接InputStreamReaderでラップして読み取り");
		final FileInputStream fis0 = new FileInputStream(filePath);
		final InputStreamReader isr0 = new InputStreamReader(fis0, Charset.defaultCharset());
		final StringBuilder sb0 = new StringBuilder();
		int i0;
		while ((i0 = isr0.read()) != -1) {
			sb0.append((char) i0);
		}
		isr0.close();
		printMsecDelta();
		
		saveMsec("StringBuilderの中身をStringに変換");
		final String str0 = sb0.toString();
		printMsecDelta();
		System.out.println("Stringの長さは " + str0.length());

		saveMsec("BufferedInputStreamでラップしてからInputStreamReaderでラップして読み取り");
		final FileInputStream fis1 = new FileInputStream(filePath);
		final BufferedInputStream bis1 = new BufferedInputStream(fis1);
		final InputStreamReader isr1 = new InputStreamReader(bis1, Charset.defaultCharset());
		final StringBuilder sb1 = new StringBuilder();
		int i1;
		while ((i1 = isr1.read()) != -1) {
			sb1.append((char) i1);
		}
		isr1.close();
		printMsecDelta();
		
		new File(filePath).delete();
	}
	
	private static void saveMsec(String m) {
		msg = m;
		msec = System.currentTimeMillis();
	}
	
	private static void printMsecDelta() {
		final long now = System.currentTimeMillis();
		System.out.println(msg + String.format(" (start: %d, end: %d, delta: %d)", msec, now, now - msec));
	}

}

実験の結果

実行してみると1回目の実行はたしかにBufferedInputStreamを使用している側が優良な成績となるけれど、2回目・3回目にはBufferedInputStreamを使わない側の成績が驚くべき改善をして、結果は同等となる(表中の数値の単位はいずれもミリ秒)。

(表1) BufferedInputStreamの使用なし

1回目 2回目 3回目
第1セット 6695 4121 4170
第2セット 6499 4092 4082
第3セット 6805 4218 4169

(表2) BufferedInputStreamの使用あり

1回目 2回目 3回目
第1セット 4295 4097 4062
第2セット 4231 4145 4120
第3セット 4282 4291 4372

BufferedInputStreamがメカニズムとして効率的なのは明らかでそれは数値にも表れている。一方BufferedInputStreamを使用しない場合でも、2回目以降、Java仮想マシンレベルで何かしら動的最適化のしくみがはたらくらしい。試しにファイルの名称を都度変更するなどしたバージョンでも同様に2回目以降の性能改善が見られたので、おそらくキャッシングのようなしくみではなく、中間コードやその実行方法について何かしらの変更をかけるしくみであるらしい。

追記:BufferedReaderでラップしたほうが効率がいい

その後ふとBufferedReaderの存在を思い出して実験してみたところ、FileOutputStream→BufferedInputStream→InputStreamReaderの順でラップを行うよりも、FileOutputStream→InputStreamReader→BufferedReaderの順でラップしたほうが効率がいいことがわかった。

FileOutputStream→InputStreamReader
バッファリングなし。2回目以降の実行時には3割ほど効率が改善する。
FileOutputStream→BufferedInputStream→InputStreamReader
InputStreamレベルのバッファリング。前者より3割ほど効率がよいが、2回目以降はほぼ同等となる。
FileOutputStream→InputStreamReader→BufferedReader
Readerレベルでバッファリング。バッファリングしない場合よりも5割ほど効率がよい。2回目以降バッファリングなしのバージョンの性能が改善してもせいぜい3割止まりなのでなお優位。