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回繰り返す:
- "あいうえお…"の羅列で300MB強のファイルを作成
- FileInputStreamをBufferedInputStreamでラップせず直接InputStreamReaderでラップしてファイル内容をすべて読み込む。
- 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割止まりなのでなお優位。