Kengo's blog

Technical articles about original projects, JVM, Static Analysis and TypeScript.

出力を高速化するBufferedOutputStreamとDeflaterOutputStream

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:蓄積する量はコンストラクタで指定可能