まだGZIPで消耗してるの!?
というアオリでスタートする今日の記事です。
はい。もうみんな使ってますねZstandard。
gzipにほぼ全方面で勝っており、使わない理由がありません。
もちろんJavaでも使えます。JNIですが。
このzstd-jniですが、圧縮に関して
- Java標準のストリーム形式(OutputStream)→少しずつ書き込む*1
byte[]
を渡してbyte[]
を返す(com.github.luben.zstd.Zstd#compress(byte[])
あたり) → 一気に書き込む
の2つのインタフェースがあります。それぞれ用途は異なりますが、明らかに後者が速い。使い方次第ですが2倍以上。 ネイティブメソッド呼び出しのオーバーヘッドは大きい*2というわけです。
でも、byte[]
のインタフェースだとギガバイトクラスのデータを圧縮する時、一旦メモリに全部置くんかい!って話になります。確かに。
というわけで簡易な便利クラスを作りました。何なのかはコードを読んでください。解説はしません。
import java.io.IOException; import java.io.OutputStream; import org.apache.commons.io.output.ByteArrayOutputStream; public class LimitedBufferedOutputStream<T extends OutputStream> extends OutputStream { private final int limit; private final OutputStream sink; @FunctionalInterface public interface OpenStream<T extends OutputStream> { T open(OutputStream out) throws IOException; } private final OpenStream<T> openStream; @FunctionalInterface public interface FinishStream<T extends OutputStream> { void finish(ByteArrayOutputStream buf, OutputStream out) throws IOException; default void finish(final T out) throws IOException {} } private final FinishStream<T> finishStream; private ByteArrayOutputStream buf = new ByteArrayOutputStream(); private T out; protected LimitedBufferedOutputStream(final OutputStream sink, final OpenStream<T> openStream, final FinishStream<T> finishStream, final int limit) { this.sink = sink; this.openStream = openStream; this.finishStream = finishStream; this.limit = limit; } public static <T extends OutputStream> LimitedBufferedOutputStream<T> of(final OutputStream sink, final OpenStream<T> openStream, final FinishStream<T> finishStream) { return of(sink, openStream, finishStream, 1024 * 1024 * 128); } public static <T extends OutputStream> LimitedBufferedOutputStream<T> of(final OutputStream sink, final OpenStream<T> openStream, final FinishStream<T> finishStream, final int limit) { return new LimitedBufferedOutputStream<>(sink, openStream, finishStream, limit); } @Override public void write(final int b) throws IOException { this.write(o -> o.write(b)); } @Override public void write(final byte[] b) throws IOException { this.onDemandFinishBuffer(b.length); this.write(o -> o.write(b)); } @Override public void write(final byte[] b, final int off, final int len) throws IOException { this.onDemandFinishBuffer(len); this.write(o -> o.write(b, off, len)); } @FunctionalInterface private interface WriteOperation { void accept(OutputStream out) throws IOException; } private void write(final WriteOperation opr) throws IOException { if (this.out != null) { opr.accept(this.out); return; } opr.accept(this.buf); this.onDemandFinishBuffer(0); } private void onDemandFinishBuffer(final int wrote) throws IOException { if ((this.buf == null) || (this.buf.size() + wrote) <= this.limit) { return; } this.out = this.openStream.open(this.sink); this.buf.writeTo(this.out); this.buf = null; } @Override public void flush() throws IOException { if (this.out != null) { this.out.flush(); } } private boolean finished; public void finish() throws IOException { if (this.finished) { return; } else { this.finished = true; } if (this.out != null) { this.finishStream.finish(this.out); } else { this.finishStream.finish(this.buf, this.sink); this.buf = null; } } @Override public void close() throws IOException { this.finish(); if (this.out != null) { this.out.close(); } else { this.sink.close(); } } }
使い方ですが、こんな感じです。
LimitedBufferedOutputStream.of( out, ZstdOutputStream::new, (b, o) -> o.write(ZSTD.compress(b.toByteArray())) );