Javaで入出力のコードを書く場合、必ずと行ってもいいほどBufferedOutputStream, BufferedInputStream, BufferedWriter, BufferedReaderのいずれかを使うのではないでしょうか。これらはJavaヒープ上にデータを蓄積*1し、I/Oの回数を削減する機能を担うものです。これを使用することでパフォーマンスを大きく向上できます。
入出力を減らすという観点からは、データを圧縮するjava.util.zipパッケージのクラス群もパフォーマンス向上に役立ちそうです。計算量は増えますが、全体として高速になるケースも少なくないでしょう。
これらのクラスがどの程度の高速化を実現できるか、サンプルで確認してみましょう。また、2つを組み合わせる上で注意すべき点にも注目します。
検証内容
今回実装したサンプルは末尾に記載していますが、端的に言えば1,000KBのデータを一時ファイルに書き込む際の所要時間を測るものです。データ出力中を行うループには明示的なインスタンス生成やメソッド実行がなく、純粋にOutputStream群の性能を測ることができます。
実行結果と考察
手元の環境(MacBookPro5,5/JVM1.6.0)では以下のような結果になりました。
FileOutputStreamをそのまま使った場合は3秒もかかっていた処理が、BufferedOutputStreamを使用するだけで約58倍も速くなっています。さらに2つのOutputStreamの間にDeflaterOutputStreamを挟むと、約128倍の高速化が確認できました。
No. | デコレート内容 | 所要時間[ミリ秒] |
---|---|---|
0 | デコレートなし | 3,463 |
1 | BufferedOutputStreamでデコレート | 60 |
2 | DeflaterOutputStreamでデコレート | 891 |
3 | BufferedOutputStreamでデコレートし、さらにDeflaterOutputStreamでデコレート | 754 |
4 | DeflaterOutputStreamでデコレートし、さらにBufferedOutputStreamでデコレート | 27 |
興味深いのはNo.3の結果です。DeflaterOutputStreamのみを使用した場合(No.2)に比べて約1.2倍の高速化しか実現できていません。デコレートする順番を変えたNo.4の方が約33倍も速いのです。この原因はDeflaterOutputStreamの実装にあると思われますが、詳細の検証は後日に回します。
結論
BufferedOutputStreamを使用することで、確かに大きなパフォーマンス向上が認められました。それほどではないとしても、DeflaterOutputStreamにもパフォーマンス向上効果が期待できます。
これらを組み合わせて使用する場合は、デコレートする順番に配慮する必要性が認められました。
サンプルコード(参考)
今回の測定で使用したコードです。mainメソッドの見通しの良さを実装方針とし、リフレクションを使用しました。例外処理が雑なのはテスト用ということでご容赦ください。
import java.io.BufferedOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FilterOutputStream; import java.io.IOException; import java.io.OutputStream; import java.lang.reflect.InvocationTargetException; import java.util.Arrays; import java.util.zip.DeflaterOutputStream; public final class WriterSpeedTest { private static final int TEST_SIZE = 1000 * 1024; @SuppressWarnings("unchecked") public static void main(String[] args) { final WriterSpeedTest tester = new WriterSpeedTest(); try { tester.test(); tester.test(BufferedOutputStream.class); tester.test(DeflaterOutputStream.class); tester.test(BufferedOutputStream.class, DeflaterOutputStream.class); tester.test(DeflaterOutputStream.class, BufferedOutputStream.class); } catch (Throwable t) { t.printStackTrace(); } } void test(final Class<? extends FilterOutputStream>... streams) throws IOException, InstantiationException, IllegalAccessException, InvocationTargetException, IllegalArgumentException, SecurityException, NoSuchMethodException { final File file = File.createTempFile("speedTest", "txt"); OutputStream stream = null; final long elapsedTime; System.out.println(Arrays.toString(streams)); try { file.deleteOnExit(); stream = createDecoratedStream(file, streams); final long startTime = System.currentTimeMillis(); for (int i = 0; i < TEST_SIZE; ++i) { stream.write(i); } elapsedTime = System.currentTimeMillis() - startTime; } finally { if (stream != null) { try { stream.close(); } catch(IOException t) { t.printStackTrace(); } } file.delete(); } System.out.println(elapsedTime); } private OutputStream createDecoratedStream(final File file, final Class<? extends FilterOutputStream>... streams) throws InstantiationException, IllegalAccessException, InvocationTargetException, FileNotFoundException, IllegalArgumentException, SecurityException, NoSuchMethodException { OutputStream stream = new FileOutputStream(file); for (Class<? extends OutputStream> decorateClass : streams) { final OutputStream decorated = decorateClass.getConstructor(OutputStream.class).newInstance(stream); stream = decorated; } return stream; } }
*1:蓄積する量はコンストラクタで指定可能