「Snappy」はGoogle社が開発した高速圧縮/伸長(解凍)ライブラリである。もともとは「Zippy」という名称で知られていたものだが、BSD Licenseに基づいてオープンソース化され、誰でも利用できるようになった。ZippyはBigTableやMapReduceから内部的なRPC処理にいたるまで、Google社内のあらゆるプロジェクトで利用してきた実績を持つ。

Snappyの最大の特徴は、圧縮/伸張の実行速度である。シングルコアのIntel Core i7プロセッサにおけるベンチマークでは、圧縮で毎秒250MB以上、伸張で毎秒500MB以上の性能を実現しているという。一方で圧縮率はzlibやLZOといった主要なツールよりも劣る。圧縮率はプレーンテキストで1.5から1.7倍、HTMLで2から4倍、JPEGやPNGといった圧縮済みのファイルフォーマットでは1.0倍となっており、これは他の圧縮ライブラリよりも20%から100%ほど大きいそうである。また、Snappyは64-bit x86プロセッサ向けに最適化されているため、それ以外の環境ではより処理速度が遅くなる可能性があるとのことだ。

圧縮/伸張の処理が速ければ、あらゆる用途においてデータの圧縮を行うことができるので、ディスク容量やネットワーク帯域への負荷を減らすことができる。圧縮率を犠牲にしてでも、処理速度を確保した方が、全体のパフォーマンスを上げられるケースもある。それに加えて、Googleのプロダクション環境で利用されてきた安定性に対する実績や、不正なバイト列が入力されてもクラッシュしない堅牢性も、Snappyの強みとして挙げられている。大容量のデータを扱うシステムや、応答速度が重視されるシステムなどでは有効な選択肢となりうるだろう。

SnappyをJavaで利用する

SnappyはC++で作られたライブラリだが、これをJavaで利用できるようにした「snappy-java」が有志の手によって公開されている。snappy-javaではJNI(Java Native Interface)を利用してネイティブのSnappyをラッピングし、JavaクラスによってSnappyの圧縮/伸張を行えるようにしたライブラリである。JNIを使うため環境に依存することになるが、Window、Mac OS、Linuxの各プラットフォームに対応している。

プロジェクトサイトに記載されているリポジトリよりjar形式のアーカイブがダウンロードできるので、使用する場合にはこれをクラスパスに含めてコンパイル・実行すればよい。

snappy-javaによる圧縮および伸張の基本となるクラスはorg.xerial.snappy.Snappyである。このクラスには圧縮を行うためのcompress()メソッドと、伸張を行うためのuncompress()メソッドが用意されている。compress()メソッドでは、次のようにbyte配列やStringオブジェクトなどを渡すことで、渡したデータの圧縮を行うことができる。byte以外のプリミティブ型の配列にも対応している。戻り値は圧縮されたデータのbyte配列である。

String text = "...";
byte[] compressed;
compressed = Snappy.compress(text.getBytes("UTF-8"));  // byte配列を渡して圧縮
compressed = Snappy.compress(text);                // 文字列を渡して圧縮
compressed = Snappy.compress(text, "UTF-8");  // 文字列を渡し、文字コードを指定して圧縮

伸張はuncompress()メソッドを用いて次のように行う。引数に渡している変数compressは、圧縮されたデータを格納したbyte配列である。戻り値には伸張後のデータがbyte配列として返される。

byte[] uncompressed = Snappy.uncompress(compressed);  // 伸長

その他に、戻り値をbyte配列ではなく他のプリミティブ型の配列やStringオブジェクトとして返すuncompressInt()やuncompressString()などといったメソッドもある。

以下のコードは、Snappyクラスを用いて文字列の圧縮および伸張を行うプログラムの例である。

public class SnappySample {
  public static void main(String[] args) {
    String text = "これはSnappy-Javaを用いて圧縮・伸長を行うプログラムのためのテスト文字列です。"
        + "SnappyはGoogleが開発した高速な圧縮・伸長ライブラリです。"
        + "Snappy-JavaはSnappyをJavaプログラムで使うためのバインディグライブラリです。";


    try {
      byte[] compressed = Snappy.compress(text, "UTF-8");  // 圧縮
      System.out.println(new String(compressed));


      byte[] uncompressed = Snappy.uncompress(compressed);  // 伸長
      System.out.println(new String(uncompressed, "UTF-8"));
    } catch (UnsupportedEncodingException ex) {
      ex.printStackTrace();
    } catch (SnappyException ex) {
      ex.printStackTrace();
    }
  }
}

実行すると、テキストを圧縮した後のバイト列(を無理やりStringにしたもの)と、それをさらに伸張して元に戻したテキストが表示される。Snappyのアルゴリズムは辞書圧縮をベースとしたものであり、基本的な考え方は、同じバイト列が現れたらそれを短く置き換えるというものである。圧縮後のデータを見るとその様子がよく分かる。

Snappyの圧縮/伸張をストリームで利用する

snappy-javaには、入出力ストリームとして圧縮/伸張を利用するクラスも用意されている。圧縮はorg.xerial.snappy.SnappyOutputStream、伸張はorg.xerial.snappy.SnappyInputStreamである。使い方は通常の入出力ストリームと同様だが、SnappyOutputStreamは出力時にSnappyによる圧縮を、SnappyInputStreamは入力時に伸張を行う。

次のプログラムは、alice29.txtというファイルを読み込み、圧縮した結果をcompressed.snappyというファイルに保存するという例である。

public class SnappyOutputStreamSample {
  public static void main(String[] args) {
    BufferedInputStream in = null;
    SnappyOutputStream out = null;
    try {
      in = new BufferedInputStream(new FileInputStream("alice29.txt"));
      out = new SnappyOutputStream(
              new BufferedOutputStream(new FileOutputStream("compressed.snappy")));
      byte[] buf = new byte[32768];
      int size=0;
      while ((size = in.read(buf, 0, buf.length)) != -1) {
        out.write(buf, 0, size);
      }
    } catch (IOException ex) {
      ex.printStackTrace();
    }
    finally {
      try {
        if (in != null) { in.close(); }
        if (out != null) { out.close(); }
      }
      catch (IOException ex) {
        ex.printStackTrace();
      }
    }
  }
}

圧縮に成功するとcompressed.snappyというファイルが生成される。続いて、これを読み込んで伸張し、結果をuncompressed.txtとして保存するプログラムの例を以下に示す。

public class SnappyInputStreamSample {
  public static void main(String[] args) {
    SnappyInputStream in = null;
    BufferedOutputStream out = null;
    try {
      in = new SnappyInputStream(
              new BufferedInputStream(new FileInputStream("compressed.snappy")));
      out = new BufferedOutputStream(new FileOutputStream("uncompressed.txt"));
      byte[] buf = new byte[32768];
      int size=0;
      while ((size = in.read(buf, 0, buf.length)) != -1) {
        out.write(buf, 0, size);
      }
    } catch (IOException ex) {
      ex.printStackTrace();
    }
    finally {
      try {
        if (in != null) { in.close(); }
        if (out != null) { out.close(); }
      }
      catch (IOException ex) {
        ex.printStackTrace();
      }
    }
  }
}

正しく実行できていれば、uncompressed.txtは元のalice29.txtと同じ内容になるはずである。

本稿ではsnappy-javaのパフォーマンスについては特に触れないが、このページにベンチマーク結果が掲載されているので、実用性を調べたい場合には参考にするといいだろう。(JNIを利用しているとはいえ)JVM上で実行されていることを考慮すれば、高いレベルのパフォーマンスを実現できていることが分かる。