#概要
以前どこかでFileInputStreamをBufferedStreamにすると処理が早くなるという記事を見つけました。
そこで今回はFileInputStreamとBufferedInputStreamについて記事を描いてみようと思います。
2019/10/06 追記
コメントにて、なぜInputStreamReaderを使用する場合はFileInputStreamでも処理が高速化されるのかを解説していただきました。
ぜひご参照ください。
#二つの違いは?
両者の違いは以下を読むとわかりやすかったです。
これは、ディスクを使用して1バイトを読み取るOSへのネイティブ呼び出しです。これは重い操作です。
BufferedInputStreamを使用すると、このメソッドは、8192バイトのバイトを読み込み、必要になるまでバッファリングする、オーバーロードされたread()メソッドに委譲します。依然として1バイトしか返されません(ただし、他のバイトは予約されています)。このようにして、BufferedInputStreamはOSにネイティブ呼び出しを少なくしてファイルから読み込みます。
BufferedInputStreamを使用してFileInputStreamを使用するよりも速くバイト単位でファイルを読み取るのはなぜですか?
つまり、FileInputStreamは一度に1バイトしか読み取りを行わないため多数のディスクアクセスが発生するのに対し、BufferedInputStreamは一度に大量のバイト数を読みとるためより少ないディスクアクセスでデータの読み取りが可能であるということです。
実際に実験して見た結果が以下です。
まず、読みとるファイルを以下のようにして作成します。
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 < 500000; i++) {
pr.println("あ");
}
pr.close();
処理1としてFileInputStreamで上記のファイルを読み取った後に、読み取ったものをStringBuilderにappendする処理をやりました。
//FileInputStream
StringBuilder sb = new StringBuilder();
final FileInputStream inputStream = new FileInputStream(filePath);
//処理
long startTime = System.currentTimeMillis();
int line;
while (true) {
line = inputStream.read();
if (line == -1) {
break;
}
sb.append(line);
}
long endTime = System.currentTimeMillis();
System.out.println("処理時間 : " + (endTime - startTime));
inputStream.close();
処理2ではFileInputStreamをBufferedInputStreamでラップして使用しました。
//BufferedInputStream
StringBuilder sb = new StringBuilder();
final FileInputStream fis = new FileInputStream(filePath);
BufferedInputStream inputStream = new BufferedInputStream(fis);
//処理
long startTime = System.currentTimeMillis();
int line;
while (true) {
line = inputStream.read();
if (line == -1) {
break;
}
sb.append(line);
}
long endTime = System.currentTimeMillis();
System.out.println("処理時間 : " + (endTime - startTime));
inputStream.close();
fis.close();
結果は以下の通りです(単位はms)。
####FileInputStream
1回目: 3840
2回目: 3820
3回目: 3772
####BufferedInputStream
1回目: 109
2回目: 111
3回目: 117
一目瞭然ですね。明らかにBufferedInputStreamの方が高速であることがわかります。
ちなみにFor文の回数を50回にしたところ、FileInputStreamでは2761196ns、BufferedInputStreamでは2195839nsとあまり変わらない数値となりました。
#InputStreamReaderを使えば違いはない?
読み込んだファイルのバイト列を、文字に変換するにはInputStreamReaderクラスを使用しますが、実はInputStreamReaderを使用することによってFileInputStreamとBufferedInputStreamの違いはほとんど無くなってしまうことがわかりました。
//FileInputStream + InputStreamReader
StringBuilder sb = new StringBuilder();
final FileInputStream inputStream = new FileInputStream(filePath);
final InputStreamReader reader = new InputStreamReader(inputStream);
//処理
long startTime = System.currentTimeMillis();
int line;
while (true) {
line = reader.read();
if (line == -1) {
break;
}
sb.append(line);
}
long endTime = System.currentTimeMillis();
System.out.println("処理時間 : " + (endTime - startTime));
inputStream.close();
reader.close();
//BufferdInputStream + InputStreamReader
StringBuilder sb = new StringBuilder();
final FileInputStream inputStream = new FileInputStream(filePath);
final BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
final InputStreamReader reader = new InputStreamReader(bufferedInputStream);
//処理
long startTime = System.currentTimeMillis();
int line;
while (true) {
line = reader.read();
if (line == -1) {
break;
}
sb.append(line);
}
long endTime = System.currentTimeMillis();
System.out.println("処理時間 : " + (endTime - startTime));
inputStream.close();
reader.close();
結果は以下の通りです(単位はms)。
####FileInputStream + InputStreamReader
1回目: 114
2回目: 131
3回目: 154
####BufferedInputStream + InputStreamReader
1回目: 163
2回目: 167
3回目: 150
BufferedInputStreamのインスタンスの生成時間があるからでしょうか、結果だけ見るとInputStreamReaderを使用する際はFileInputStreamをラップしないでファイル読み取りをやる方が早いように見えますね。
読み取ったファイルを文字列としてStringBuilderなどに出力する場合は、InputStreamReaderを使うことになると思いますので、FileInputStreamでもBufferedInputStreamでもあまり変わらないということになりますね。
InputStreamReaderの説明については以下を参照ください。
・InputStreamReaderとは : JavaA2Z
#じゃあ常にBufferedInputStreamを使えばよいのか?
明らかにBufferedInputStreamの方が処理スピードが早いので使った方がよいと思われるかもしれませんが、実は使ってもあまり効果がないという場合が存在します。
それはreadメソッドの引数にバッファサイズを設定したときです。
InputStreamではread()の引数としてバッファサイズを設定することができ、一度に読みとるバイト数を制御することが可能です。
何も設定していない場合は一度に1バイトずつの読み取りとなってしまいます(BufferedInputStreamは例外ですが)。
ですので、例えば上記処理1や処理2のコードを以下のように書き換えるならば、速度的な面で言えば、あまり変わらなくなってしまいます。
//処理1 FileInputStream
StringBuilder sb = new StringBuilder();
final FileInputStream inputStream = new FileInputStream(filePath);
// BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
long startTime = System.currentTimeMillis();
int line;
while (true) {
line = inputStream.read(new byte[8192]);
if (line == -1) {
break;
}
sb.append(line);
}
long endTime = System.currentTimeMillis();
System.out.println("処理時間 : " + (endTime - startTime));
inputStream.close();
read(new byte[8192])として、FileInputStreamとBufferedInputStreamの両方で処理を行ったところ、両者とも実行時間は3msとなり、結果的にあまり変わらない速さとなりました。
これについては以下のサイトの記載が参考になったので引用します。
大量の小さな読み取り(一度に1バイトまたは数バイト)を実行する可能性がある場合、またはバッファーAPIによって提供される高水準の機能を使用したい場合は、意味があります。例えばBufferedReader.readLine()メソッドです。
ただし、read(byte [])メソッドまたはread(byte []、int、int)メソッドを使用して大きなブロックの読み込みのみを実行する場合は、BufferedInputStreamでInputStreamをラップしても効果がありません。
後は、上記しましたが、入力するファイルサイズが極端に小さい場合もどちらを使っても速度的にはあまり変わらないのでラップする効果は薄いです。
#まとめ
この記事を書く前は「FileInputStreamよりBufferedInputStreamの方が絶対速いだろう」と思っていたのですが、InputStreamReader(BufferedInputSteramReader)を使うと、両者はあまり違わないことがわかりました。
また、BufferedInputStreamを使用してもあまり効果がない場合が存在することも勉強になりました。
どんなクラスでもそうですが、時と場合に合わせて適切なクラスを選択することが大切になりそうですね。
その他参考:
BufferedInputStreamの威力を実験してみる
FIO10-J. read() を使って配列にデータを読み込むときには配列への読み込みが意図した通りに行われたことを確認する