seraphyの日記

日記というよりは過去を振り返るときのための単なる備忘録

jmhマイクロベンチマークツールを使ったByteBufferのバイトオーダによるパフォーマンスの違いの比較

(ヒープおよびダイレクトバッファによるByteBufferの性能差、およびUnsafeとのパフォーマンスも比較するよ!)
(とりあえず計測結果を先に知りたい人は結論を見てね!)
(ファイルのシーケンシャルリードを含むダイレクトバッファの使い方やパフォーマンス比較については次の日記に書きました。)

概要

思うところあって、ヒープ上のbyte[]をラップしたByteBufferと、ダイレクトバッファを確保しているByteBufferの場合とのint型としての連続アクセスした場合のパフォーマンスの違いについて調べてみることにした。


私の認識では、ダイレクトバッファへのアクセスは非ネイティブ側(つまりJavaコード側)からのアクセスではオーバヘッドが大きいため、特殊なシチュエーション以外では遅くなってしまうため、あまり使いどころのない機能だというものだったのだが、では、その使いどころとなる「特殊なシチュエーション」とは何か、ということについては、いまいちわからなかった。

とくに、ByteBufferを連続的にintアクセスするような用途には、ヒープ上のバッファよりも、むしろ遅くなるのではないか?という漠然とした印象があった。


だが、実際にためしたところ、予想外にも、"ダイレクトバッファはリトルエンディアンのint型をバイト列から読み取るにはヒープ上のバッファよりもものすごく良いパフォーマンスが得られるかも?"という、個人的には非常に興味深い示唆に富む結果を得ることができた。
そこで、ベンチマークツールの使い方の備忘録も兼ねて、ここに記録しておくことにする。


※ なお、JAVAベンチマークは条件が少しでも変われば結果が大きく変わったりするので、あくまでも、「私が試したケース」ではという前提であり、また、どうして、このような結果になったかについては納得のゆく確証が得られていないため、もし、この記事を参考にすることがあるのならば、これを鵜呑みにせず、実際には個々のケースに応じてベンチマークをとるべきだろう。
(その際には、ここで紹介するベンチマークツールjmhが有益だと思われる。)

ベンチマークツールの準備

今回は、ベンチマークツールとして "jmh"を久しぶりに使うことにした。

このツールは、「まともに作るのが案外難しい」ベンチマークコードの作成をアシストして、さらにベンチマーク結果の統計情報(標準偏差や信頼区間)も計算して出してくれるようにしてくれる、大変便利なベンチマークジェネレータツールである。


1年以上つかってなかったのだが、久しぶりに使ってみたら、ちょっと変化があって、古いコードとは、ほんの少し書き方が変わっているようだ。
ネット上の資料でも古い書き方をしているものがあるので、その点は最新の書き方に改める必要がある。
とはいえ、基本的な考え方や使い方などは変わっていない。


jmhの概要や使い方については、少し古い記事だが以下のページが大変参考になる。

(あとは公式ページと、沢山ある付属サンプルから、なんとかする。)

jmhとは?

jmhは、openjdkで開発されている、java用のマイクロベンチマークツールである。

http://openjdk.java.net/projects/code-tools/jmh/


JAVAではコンパイル時最適化や実行時最適化によってソースコード上から予想される動きよりも、はるかに最適化された挙動を示すことが多々ある。

パフォーマンス比較するつもりで書いた単純なコードが実運用ではありえない形にアグレッシブに最適化されたり、あるいは逆に、通常ではJITによる最適化が行われるべきところをベンチマークの実行が浅くて最適化されてない状態で比較する、もしくは、評価したい部分とは関係ない部分でのパフォーマンスを計測に含んでしまうなど、頓珍漢なパフォーマンス比較になっていたりしたら、大変な判断ミスを犯すことになるだろう。

このような最適化がされるような状況でベンチマークをとるのは、めんどくさいことは容易に想像できる。


これらを比較的容易にベンチマークをとれるようにお膳立てしてくれるのが、jmhである。


参考:


ただし、jmhはjunitのようなテストライブラリではない。


jmhは、マイクロベンチマークを行うためのソースコードを自動生成して、その自動生成されたテストコードの上から、ユーザーが定義したベンチマークコードを実行するための、実行可能jarを生成する、いうなれば「テストベンチジェネレータ」ツールなのである。

ベンチマークプロジェクトの準備

jmhを使うのは簡単で、mavenから新規アーキタイプとして"jmh-java-benchmark-archetype"を指定してプロジェクトを作成すれば、必要なものは自動的に手に入るようになっている。

<dependency>
	<groupId>org.openjdk.jmh</groupId>
	<artifactId>jmh-java-benchmark-archetype</artifactId>
	<version>1.11.2</version>
</dependency>

最近のLunaとかMarsあたりのEclipseであればmavenは初めから使えるようになっている。*1



初期状態では、このアーキタイプはローカルマシン上のMavenリポジトリには登録されていないと思われるので、まずは、アーキタイプを登録しておく。(登録後、一度ダイアログを閉じて再度新規プロジェクト作成を開きなおすと、一覧に出てくるようになる。)


※ ここでは現時点の最新であるバージョン1.11.2を使っている。(最新はリポジトリを確認のこと。)



こうしてプロジェクトが作成されるので、まずはpom.xmlを修正して、
java8をターゲットに変えておく。

※ 当然ながら、javaのバージョンが違えばパフォーマンスも変わってくるだろう。



(pom.xmlの抜粋)

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <jmh.version>1.11.2</jmh.version>
        <javac.target>1.8</javac.target>
        <uberjar.name>benchmarks</uberjar.name>
    </properties>
空のベンチマークの実行

初期状態で生成されたクラスは空のベンチマークであるが、ビルドすればパフォーマンスの計測は可能である。

package jp.seraphyware;

import org.openjdk.jmh.annotations.Benchmark;

public class MyBenchmark {

    @Benchmark
    public void testMethod() {
        // This is a demo/sample template for building your JMH benchmarks. Edit
        // as needed.
        // Put your benchmark code here.
    }
}

プロジェクト内にあるクラスがスキャンされ、@Benchmarkアノテーションがついているすべてのメソッドがベンチマークメソッドとして認識される。

(※ ちなみに、以前は@Benchmarkではなく、@GenerateMicroBenchmarkというアノテーションが使われていたように思う。)


ただし、このベンチマークを実行するためには、まずはmavenによるビルドが必要である。


先に説明したとおり、jmhはライブラリではなく、テストベンチジェネレータであり、mavenでビルドすることによりさまざまなファイルを自動生成する。
これらのファイルがないとベンチマークを動かすことができない。

mvn clean package



(画面キャプチャを取り損ねたので素のEclipseで撮りなおしています。)


ビルドが完了すると、targetフォルダ上に実行可能なjar"benchmarks.jar"が生成されている。

コマンドプロンプトを開き、これを実行することができる。


以下はコマンド実行と、その結果である。

>set PATH=C:\java\jdk1.8.0_60\bin;%PATH%
>java -jar benchmarks.jar "MyBenchmark.testMethod" -f1 -wi 1 -i 1
# JMH 1.11.2 (released 15 days ago)
# VM version: JDK 1.8.0_60, VM 25.60-b23
# VM invoker: C:\java\jdk1.8.0_60\jre\bin\java.exe
# VM options: <none>
# Warmup: 1 iterations, 1 s each
# Measurement: 1 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: jp.seraphyware.MyBenchmark.testMethod

# Run progress: 0.00% complete, ETA 00:00:02
# Fork: 1 of 1
# Warmup Iteration   1: 3680300939.059 ops/s
Iteration   1: 3687070753.959 ops/s


Result "testMethod":
  3687070753.959 ops/s


# Run complete. Total time: 00:00:02

Benchmark                Mode  Cnt           Score   Error  Units
MyBenchmark.testMethod  thrpt       3687070753.959          ops/s

>

オプション引数は"-h"で確認できる。


上記の場合の引数は、テストする「クラス.メソッド名」を明示して、ウォームアップイテレーションx1, イテレーションx1, フォークx1、という指定である。
(いうまでもなく、これは単なる動作確認で、計測するには不十分な回数である。)


また、テストするクラス・メソッド名は正規表現で判断されるので、この場合、たとえば、"MyBenchmark"としても、それにマッチするすべてのベンチマークが実行されることになる。(何も指定しないと、すべてのベンチマークメソッドが実行される。)


実行結果の「Score 3687070753.959 ops/s」というのは、デフォルトのテストモードが「スループット計測モード」であるため、これは1秒間に何回テストベンチを処理できたかの回数を表すものである。
(1イテレーションベンチマークはデフォルトでは1秒間となっている。)
(ベンチマークメソッドが空なので、ものすごい回数が回っている。)


今回はイテレーション数が1のためベンチマーク実行回数が1回しかないため表示されていないが、
複数回のイテレーションを行った場合には、最小値、最大値、平均値、標準偏差、99.9%信頼区間などの数値が計算される。

IDEからの実行

一度mavenでビルドすると、テストベンチ用のソースコードも生成されているのでEclipseからも実行できるようになっている。

Mainメソッドとして、ベンチマークを起動するために、以下のようなものを追加する。

    /**
     * IDEから実行する場合は、
     * まず先にmvn package等を実行してサポートファイル類を生成しておく必要がある.
     * (少なくともクラスまたはメソッドのシグネチャを変更追加するたびにmvnでビルドする必要がある。)
     * その後、IDEから、このメイン関数を呼び出すことができるようになる.
     * @param args
     * @throws RunnerException
     */
    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                // 実行すべきベンチマークが定義された"クラス名.メソッド名"を正規表現で指定
                .include(MyBenchmark.class.getSimpleName())
                // ウォームアップイテレーション数 "-wi"オプションに相当
                .warmupIterations(20)
                // イテレーション数 "-i"オプションに相当
                .measurementIterations(10)
                // フォーク数 "-f"オプションに相当
                .forks(1)
                // 1回のテストメソッド実行時間 "-r"オプションに相当
                .measurementTime(TimeValue.seconds(1))
                // 同時実行スレッド数 (1測定毎のスレッド数 (サンプル数が増える))
                // .threads(Math.max(1, Runtime.getRuntime().availableProcessors() / 2))
                // 測定するモード = スループット
                .mode(Mode.Throughput)
                .build();
        new Runner(opt).run();
    }

これで、このクラスを実行クラスに選択すれば、ベンチマークを実行することができるようになる。


ただし、ベンチマーククラスまたはメソッド(およびメソッドの引数等のシグネチャ)を追加削除変更した場合には、再度、mavenによるビルドが必要となる。(その後、Eclipseのパッケージエクスプローラのリフレッシュを忘れずに。)


※ OptionsBuilderのincludeメソッドで実行するベンチマークの「クラス名.メソッド名」を正規表現で絞りこめるので、ベンチマークコードを試行錯誤している間は、メソッド名まで指定すると良いだろう。

ByteBufferのパフォーマンス比較コードの作成

ByteArrayには、ヒープ上のbyte[]配列をラップしたものと、ヒープ外に確保したダイレクトバッファによるものの2種類がある。


ダイレクトバッファはヒープ外にあるため、OSなどネイティブなヒープ外のコードからアクセスする場合にはオーバヘッドがないかわりに、逆に、Javaのコードからアクセスするにはオーバヘッドが生ずる。
このトレードオフを見極めるのが難しく、ダイレクトバッファを使えばなんでも速くなるけではない。(むしろ、普通のバイトバッファのように扱えば、大抵遅くなるだろう。)


ただし、この2種類はインターフェイスは共通であり、同じようなコードで扱うことができる。


今回は、バイトバッファからint値を連続して読み込むために以下のようなコードを用いる。

    /**
     * バイトバッファからint値を読み出すテスト
     * @param buf
     */
    private static void runByteBuffer(ByteBuffer buf) {
        buf.rewind();
        int limit = buf.limit();
        long total = 0;
        while (buf.position() < limit) {
            total += buf.getInt();
        }
        if (total != 8589869056L) {
            throw new RuntimeException("actual=" + total);
        }
    }

このコードは単純に、ByteBufferのバッファ上から、すべてのint値を読み込んでtotal変数に足しこんでいるだけである。

最後に、total変数を8589869056Lという謎の数値と照合させているが、これはテストデータとして用意したバイト列を正しく読み出した場合に計算される値である。


ヒープ上のbyte[]配列をByteBufferにラップしたものと、ダイレクトバッファの双方で、このコードを使って計測する。


テストデータは以下のようにして生成している。

(バッファサイズは今回は512KiBとしている。)

    /**
     * データを初期化する.
     * @param buf
     */
    private void initData(ByteBuffer buf) {
        IntBuffer intBuf = buf.asIntBuffer();
        long total = 0;
        for (int idx = 0; idx < data.length / 4; idx++) {
            intBuf.put(idx);
            total += idx;
        }
        //System.out.println("★total=" + total);
        if (total != 8589869056L) {
            throw new RuntimeException("actual=" + total);
        }
    }

テストの状態を保持する@Stateアノテーションの利用

このテストデータを生成するタイミングをコントロールするために、jmhの@Stateという仕組みを用いる。

前述のとおり、テストベンチメソッドは繰り返し大量に呼び出されるものなので、都度、テストデータを作成していては計測がおぼつかない。

このため、テストベンチに対してテストの状態を保持するインスタンスを持たせることができるようになっている。


@Stateというアノテーションがつけられたクラスを、テストベンチメソッドの引数にすることで、
jmhテストベンチによってベンチマークメソッドが呼び出されるときに自動的に、そのインスタンスが引き渡されてくるようになる。

@Stateアノテーションがつけられたクラスは、内部に「@Setup」「@TearDown」のアノテーションがついたメソッドを定義することができ、

の3種類のタイミングでメソッドを呼びすように設定できる。

(前述のとおり、テストベンチメソッドは繰り返し大量に呼び出されるものなので、Invocationの使用には注意が必要となる。)


ここで、Trialレベルの@Setupメソッドでテストデータを準備すれば、
純粋にByteBufferをint値でアクセスする部分だけを大量に繰り返しテストできるようになる。

※ なお、テストベンチはマルチスレッドでテストすることもできるので、その場合、@StateアノテーションにScope.Threadを指定することにより、スレッドスコープとしてスレッド間の衝突なく使えるようにすることができる。(このほかに、Benchmark, Groupのスコープがある。)

    /**
     * テスト中にスレッドごとベンチマークごとの状態を保持するインスタンス.
     *
     * @author seraphy
     */
    @State(Scope.Thread)
    protected static class BenchContextBase {

        /**
         * バイトオーダー
         */
        private final ByteOrder byteOrder;

        /**
         * ヒープ
         */
        private byte[] data;

        /**
         * ヒープをラップしたバッファ
         */
        private ByteBuffer bufHeap;

        /**
         * ダイレクトバッファ
         */
        private ByteBuffer bufDirect;

        /**
         * バイトオーダを指定して構築する.
         * @param byteOrder
         */
        protected BenchContextBase(ByteOrder byteOrder) {
            this.byteOrder = byteOrder;
        }

        /**
         * データの初期化.
         * Trialの場合、スレッドごとベンチマークの開始ごとに一度呼び出される.<br>
         * (ウォームアップイテレーション、および計測イテレーション中には呼び出されない。)
         * (Trialがデフォルト、そのほかにIteration, Invocationが指定可能.)
         */
        @Setup(Level.Trial)
        public void setup() {
            // ヒープ上にデータを作成する
            byte[] data = new byte[512 * 1024]; // 512kib
            this.data = data;

            // ヒープ上のByteBufferを作成する.
            bufHeap = ByteBuffer.wrap(data);
            bufHeap.order(byteOrder);
            // ※ テストデータを作成する
            initData(bufHeap);

            // ダイレクトメモリを確保してByteBufferを作成する
            // (データはヒープのものをコピーする)
            bufDirect = ByteBuffer.allocateDirect(data.length);
            bufDirect.order(byteOrder);
            bufDirect.put(data);
        }
   ... (中略) ...
   }

今回は、ByteBufferにつめるデータ形式として、JavaのデフォルトであるBigEndianと、Intel系CPUのデフォルトであるLittleEndianの2種類のデータを作成できるようにしている。(上記コード中のbyteOrder変数が、それにあたる。)

    /**
     * データの並びがLittleEndianのバッファをもつテストデータを作成する.
     */
    public static class BenchContextLE extends BenchContextBase {

        public BenchContextLE() {
            super(ByteOrder.LITTLE_ENDIAN); // Unsafe(intel)にあわせる
        }
    }

    /**
     * データの並びがBigEndianのバッファをもつテストデータを作成する.
     */
    public static class BenchContextBE extends BenchContextBase {

        public BenchContextBE() {
            super(ByteOrder.BIG_ENDIAN); // JAVA標準にあわせる
        }
    }

Unsafeによるbyte[]へのint値としての直接アクセスの実験もしてみる

今回はByteBufferでのアクセスの計測の他に、sun.misc.Unsafeというネイティブ的な動作をするAPI群があり、この中にオブジェクト上の任意のバッファをint値として読み取ることのできる機能があるらしいので、これも、ためしに使って計測してみる。


参考元:


もちろん、sun.misc.Unsafeは、非公開、非推奨のベンダ固有APIである。

OpenJDKとSun系のJDKにはとりあえず現在は存在しており、各種有名どころのライブラリも使っていたりするらしい。

(また、JavaVMまわりの制御を行えるので、特にロックまわりではsun.misc.Unsafeを使わないとどうしても実現できないようなものもあるといううわさも聞いた。)*2

    /**
     * ヒープからUnsafeを使ってint値を読み出すテスト
     * @param unsafe
     * @param data
     */
    @SuppressWarnings("restriction")
    private static void runUnsafe(sun.misc.Unsafe unsafe, byte[] data) {
        int offset = sun.misc.Unsafe.ARRAY_BYTE_BASE_OFFSET;
        int len = data.length + offset;
        int pos = offset;
        long total = 0;
        while (pos < len) {
            total += unsafe.getInt(data, (long) pos);
            pos += 4;
        }
        if (total != 8589869056L) {
            throw new RuntimeException("actual=" + total);
        }
    }

このコードはbyteを4バイト飛ばしでint値として無理やり読み込んでいるものである。

byte配列の読み出しには、オフセットとしてsun.misc.Unsafe.ARRAY_BYTE_BASE_OFFSETが必要になるらしい。*3


また、そもそもUnsafeを扱うためには、直接、Unsafeを取り出そうとするとヌルクラスローダ上のクラスからの呼び出しであるか判定して、システム外クラスからの呼び出しはセキュリティエラーにする実装となっているため、リフレクションを経由して無理やり取ってくる必要がある。

    /**
     * アンセーフ
     */
    @SuppressWarnings("restriction")
    private sun.misc.Unsafe unsafe;

    /**
     * アンセーフをリフレクションによって取得する.
     */
    @SuppressWarnings("restriction")
    private void initUnsafe() {
        try {
            Field field = sun.misc.Unsafe.class
                    .getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (sun.misc.Unsafe) field.get(null);

        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

さらに、byte[]に格納されたLittle Endianを自力でint値にしてみる

何回かBenchmarkをとったところ、予想外にヒープのByteBufferの性能が悪そうなので、比較のために、自力でbyte[]からLittle Endianのint値をアクセスするコードのベンチマークもとってみることにする。

    @Benchmark
    public void testHeapByteArray(BenchContextLE ctx) {
        byte[] data = ctx.getData();
        int len = data.length;
        int pos = 0;
        long total = 0;
        while (pos < len) {
            total += ((data[pos + 3] & 0xFF) << 24)
                    | ((data[pos + 2] & 0xFF) << 16)
                    | ((data[pos + 1] & 0xFF) << 8)
                    | (data[pos + 0] & 0xFF);
            pos += 4;
        }
        if (total != 8386560) {
            throw new RuntimeException("actual=" + total);
        }
    }

※ 参考: https://www.jpcert.or.jp/java-rules/fio12-j.html

ベンチマークメソッドの定義

これで、

  • ヒープ/ダイレクトのByteBuffer
  • BigEndian/LittleEndian
  • Unsafe#getIntによるintアクセス
  • byte[]の自力でのintアクセス

の準備が整った。

このテストベンチは以下のようになる。

    /**
     * LittleEndianのデータをヒープ上のバイトバッファから読み出すベンチマーク.
     * @param ctx
     */
    @Benchmark
    public void testHeapByteBufferLE(BenchContextLE ctx) {
        runByteBuffer(ctx.getHeapByteBuffer());
    }

    /**
     * LittleEndianのデータをダイレクトバイトバッファから読み出すベンチマーク.
     * @param ctx
     */
    @Benchmark
    public void testDirectByteBufferLE(BenchContextLE ctx) {
        runByteBuffer(ctx.getDirectByteBuffer());
    }

    /**
     * LittleEndianのデータをアンセーフを使って読み出すベンチマーク
     * @param ctx
     */
    @Benchmark
    public void testUnsafeLE(BenchContextLE ctx) {
        runUnsafe(ctx.getUnsafe(), ctx.getData());
    }

    /**
     * BigEndianのデータをヒープ上のバイトバッファから読み出すベンチマーク.
     * @param ctx
     */
    @Benchmark
    public void testHeapByteBufferBE(BenchContextBE ctx) {
        runByteBuffer(ctx.getHeapByteBuffer());
    }

    /**
     * BigEndianのデータをダイレクトバイトバッファから読み出すベンチマーク.
     * @param ctx
     */
    @Benchmark
    public void testDirectByteBufferBE(BenchContextBE ctx) {
        runByteBuffer(ctx.getDirectByteBuffer());
    }

ベンチマークの実行結果

これでベンチマークを実行したところ、以下のような結果が得られた。(抜粋)


実行したのはWindows8.1(x64)で、CPUはXeon E3-1270v2@3.5GHzの環境である。

(Windows7(x64) で、i7-3770@3.40GHzでも、ほぼ同じ結果となった。)

# JMH 1.11.2 (released 16 days ago)
# VM version: JDK 1.8.0_60, VM 25.60-b23
# VM invoker: C:\Java\jdk1.8.0_60\jre\bin\java.exe
# VM options: -Dfile.encoding=UTF-8
# Warmup: 20 iterations, 1 s each
# Measurement: 10 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: jp.seraphyware.jmhexample.BufferAccessBenchmark.testDirectByteBufferBE

Result "testDirectByteBufferBE":
  14749.779 ±(99.9%) 54.380 ops/s [Average]
  (min, avg, max) = (14690.298, 14749.779, 14806.503), stdev = 35.969
  CI (99.9%): [14695.400, 14804.159] (assumes normal distribution)

Result "testDirectByteBufferLE":
  21970.174 ±(99.9%) 154.358 ops/s [Average]
  (min, avg, max) = (21833.488, 21970.174, 22122.518), stdev = 102.098
  CI (99.9%): [21815.816, 22124.532] (assumes normal distribution)

Result "testHeapByteArray":
  4744.254 ±(99.9%) 83.476 ops/s [Average]
  (min, avg, max) = (4618.953, 4744.254, 4799.570), stdev = 55.214
  CI (99.9%): [4660.778, 4827.730] (assumes normal distribution)

Result "testHeapByteBufferBE":
  4518.875 ±(99.9%) 491.083 ops/s [Average]
  (min, avg, max) = (4124.723, 4518.875, 4919.513), stdev = 324.821
  CI (99.9%): [4027.792, 5009.958] (assumes normal distribution)

Result "testHeapByteBufferLE":
  4581.578 ±(99.9%) 618.657 ops/s [Average]
  (min, avg, max) = (4104.452, 4581.578, 4948.241), stdev = 409.203
  CI (99.9%): [3962.921, 5200.234] (assumes normal distribution)

Result "testUnsafeLE":
  21981.865 ±(99.9%) 125.801 ops/s [Average]
  (min, avg, max) = (21841.979, 21981.865, 22068.251), stdev = 83.209
  CI (99.9%): [21856.064, 22107.665] (assumes normal distribution)


# Run complete. Total time: 00:03:02

Benchmark                                      Mode  Cnt      Score     Error  Units
BufferAccessBenchmark.testDirectByteBufferBE  thrpt   10  14749.779 ±  54.380  ops/s
BufferAccessBenchmark.testDirectByteBufferLE  thrpt   10  21970.174 ± 154.358  ops/s
BufferAccessBenchmark.testHeapByteArray       thrpt   10   4744.254 ±  83.476  ops/s
BufferAccessBenchmark.testHeapByteBufferBE    thrpt   10   4518.875 ± 491.083  ops/s
BufferAccessBenchmark.testHeapByteBufferLE    thrpt   10   4581.578 ± 618.657  ops/s
BufferAccessBenchmark.testUnsafeLE            thrpt   10  21981.865 ± 125.801  ops/s


※ このベンチマークは512kibのバッファの読み込みを繰り返すものであり、上記スコア値はops/sの平均、つまり1秒間あたりの実行回数の平均なので、簡単に処理速度を求めるなら、単純に512kibをかければ良い。

他の計測モード

jmhでは計測モードとして、デフォルトのスループットの他に、平均時間、サンプルタイム、シングルショットタイムを選択できる。

  • スループット(ops/s)はベンチマークを1秒間に何回実行したかを示すものである。
  • 平均時間は、これを時間に換算したものである。(ops/sで1を割れば時間が出てくる。)
  • サンプルタイムは、実行時間の分散を調べるものであり、たとえば実行したベンチマークのうち、50%は最大で、どのぐらいの所要時間が必要だったか?また、実行したベンチマークの90%は最大で、どのくらい所要時間が必要だったか?といった値を見ることができる。また、結果としては最大、最小、平均の所要時間と標準偏差、サンプル総数などが得られるので、時間を計測するならばサンプルタイムのほうが良いかもしれない。
  • シングルショットタイムは、イテレーションではなくバッチサイズで指定し、バッチサイズ分実行したトータルタイムを出す。(単純なパフォーマンス計測に向く?)


以下はサンプルタイムで計測しなおした結果である。

計測モードとして、サンプルタイムを指定し、単位はマイクロ秒とする。
(コマンドラインから実行する場合は、オプションとして-bmオプションでモード指定する。)

Options opt = new OptionsBuilder()
    .(中略)
    .timeUnit(TimeUnit.MICROSECONDS)
    .mode(Mode.SampleTime)
    .build();

サンプルタイムの結果 (抜粋)

# JMH 1.11.2 (released 16 days ago)
# VM version: JDK 1.8.0_60, VM 25.60-b23
# VM invoker: C:\Java\jdk1.8.0_60\jre\bin\java.exe
# VM options: -Dfile.encoding=UTF-8
# Warmup: 20 iterations, 1 s each
# Measurement: 10 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Sampling time
# Benchmark: jp.seraphyware.jmhexample.BufferAccessBenchmark.testDirectByteBufferBE

Iteration   1: n = 14259, mean = 70 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 66, 68, 73, 85, 129, 164, 180, 181 us/op
Iteration   2: n = 14022, mean = 71 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 66, 68, 76, 87, 138, 176, 377, 485 us/op
Iteration   3: n = 14341, mean = 70 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 66, 67, 70, 84, 135, 175, 342, 390 us/op
Iteration   4: n = 14122, mean = 71 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 66, 68, 76, 87, 137, 172, 241, 241 us/op
Iteration   5: n = 14138, mean = 71 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 66, 68, 76, 87, 139, 165, 198, 205 us/op
Iteration   6: n = 14624, mean = 68 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 66, 66, 69, 77, 103, 145, 181, 184 us/op
Iteration   7: n = 14589, mean = 68 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 66, 66, 69, 78, 103, 153, 173, 174 us/op
Iteration   8: n = 14578, mean = 69 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 66, 66, 69, 78, 107, 148, 163, 164 us/op
Iteration   9: n = 14606, mean = 68 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 66, 66, 69, 76, 105, 147, 165, 169 us/op
Iteration  10: n = 14259, mean = 70 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 66, 68, 75, 85, 121, 155, 207, 233 us/op

Result "testDirectByteBufferBE":
  69.583 ±(99.9%) 0.088 us/op [Average]
  (min, avg, max) = (65.664, 69.583, 484.864), stdev = 10.159
  CI (99.9%): [69.495, 69.672] (assumes normal distribution)
  Samples, N = 143538
        mean =     69.583 ±(99.9%) 0.088 us/op
         min =     65.664 us/op
  p( 0.0000) =     65.664 us/op
  p(50.0000) =     67.712 us/op
  p(90.0000) =     70.912 us/op
  p(95.0000) =     83.840 us/op
  p(99.0000) =    126.492 us/op
  p(99.9000) =    162.304 us/op
  p(99.9900) =    207.104 us/op
  p(99.9990) =    443.401 us/op
  p(99.9999) =    484.864 us/op
         max =    484.864 us/op


Iteration   1: n = 10857, mean = 46 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 44, 44, 47, 53, 88, 108, 149, 149 us/op
Iteration   2: n = 10619, mean = 47 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 44, 45, 47, 55, 97, 117, 150, 151 us/op
Iteration   3: n = 10738, mean = 47 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 44, 44, 47, 55, 90, 110, 142, 143 us/op
Iteration   4: n = 10942, mean = 46 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 44, 44, 47, 47, 72, 106, 133, 133 us/op
Iteration   5: n = 10874, mean = 46 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 44, 44, 47, 53, 73, 105, 136, 136 us/op
Iteration   6: n = 10878, mean = 46 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 44, 44, 47, 53, 74, 106, 119, 119 us/op
Iteration   7: n = 10955, mean = 46 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 44, 44, 47, 48, 71, 106, 107, 107 us/op
Iteration   8: n = 10945, mean = 46 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 44, 44, 47, 47, 71, 106, 107, 108 us/op
Iteration   9: n = 10842, mean = 46 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 44, 44, 47, 54, 86, 110, 172, 174 us/op
Iteration  10: n = 10852, mean = 46 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 44, 44, 47, 53, 86, 111, 122, 122 us/op

Result "testDirectByteBufferLE":
  46.035 ±(99.9%) 0.064 us/op [Average]
  (min, avg, max) = (43.968, 46.035, 174.336), stdev = 6.423
  CI (99.9%): [45.971, 46.099] (assumes normal distribution)
  Samples, N = 108502
        mean =     46.035 ±(99.9%) 0.064 us/op
         min =     43.968 us/op
  p( 0.0000) =     43.968 us/op
  p(50.0000) =     44.224 us/op
  p(90.0000) =     46.592 us/op
  p(95.0000) =     53.312 us/op
  p(99.0000) =     81.408 us/op
  p(99.9000) =    107.520 us/op
  p(99.9900) =    131.635 us/op
  p(99.9990) =    172.333 us/op
  p(99.9999) =    174.336 us/op
         max =    174.336 us/op


Iteration   1: n = 6413, mean = 156 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 150, 151, 162, 169, 220, 326, 342, 342 us/op
Iteration   2: n = 6407, mean = 156 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 150, 151, 161, 170, 240, 321, 344, 344 us/op
Iteration   3: n = 6409, mean = 156 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 150, 151, 159, 169, 288, 343, 444, 444 us/op
Iteration   4: n = 6436, mean = 155 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 150, 151, 162, 169, 218, 313, 354, 354 us/op
Iteration   5: n = 6473, mean = 154 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 150, 151, 158, 165, 216, 333, 413, 413 us/op
Iteration   6: n = 6445, mean = 155 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 150, 151, 159, 168, 220, 317, 344, 344 us/op
Iteration   7: n = 6462, mean = 155 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 150, 151, 159, 168, 209, 322, 360, 360 us/op
Iteration   8: n = 6460, mean = 155 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 150, 151, 159, 168, 214, 316, 334, 334 us/op
Iteration   9: n = 6473, mean = 154 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 150, 151, 159, 168, 220, 319, 352, 352 us/op
Iteration  10: n = 6440, mean = 155 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 150, 151, 160, 168, 220, 325, 344, 344 us/op

Result "testHeapByteArray":
  155.058 ±(99.9%) 0.194 us/op [Average]
  (min, avg, max) = (149.504, 155.058, 444.416), stdev = 14.943
  CI (99.9%): [154.864, 155.251] (assumes normal distribution)
  Samples, N = 64418
        mean =    155.058 ±(99.9%) 0.194 us/op
         min =    149.504 us/op
  p( 0.0000) =    149.504 us/op
  p(50.0000) =    150.528 us/op
  p(90.0000) =    159.488 us/op
  p(95.0000) =    168.448 us/op
  p(99.0000) =    227.072 us/op
  p(99.9000) =    325.632 us/op
  p(99.9900) =    357.947 us/op
  p(99.9990) =    444.416 us/op
  p(99.9999) =    444.416 us/op
         max =    444.416 us/op


Iteration   1: n = 4607, mean = 217 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 207, 212, 226, 243, 359, 433, 477, 477 us/op
Iteration   2: n = 4666, mean = 214 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 207, 207, 221, 233, 330, 454, 470, 470 us/op
Iteration   3: n = 4706, mean = 212 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 207, 207, 219, 227, 311, 423, 442, 442 us/op
Iteration   4: n = 4708, mean = 212 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 207, 207, 218, 227, 306, 418, 498, 498 us/op
Iteration   5: n = 4697, mean = 213 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 207, 207, 225, 228, 289, 444, 565, 565 us/op
Iteration   6: n = 4701, mean = 213 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 207, 207, 218, 227, 314, 460, 479, 479 us/op
Iteration   7: n = 4707, mean = 212 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 207, 207, 219, 226, 295, 397, 446, 446 us/op
Iteration   8: n = 4703, mean = 212 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 207, 207, 219, 227, 307, 436, 467, 467 us/op
Iteration   9: n = 4695, mean = 213 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 207, 207, 225, 226, 339, 454, 461, 461 us/op
Iteration  10: n = 4608, mean = 217 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 207, 212, 225, 243, 362, 464, 481, 481 us/op

Result "testHeapByteBufferBE":
  213.519 ±(99.9%) 0.304 us/op [Average]
  (min, avg, max) = (206.848, 213.519, 565.248), stdev = 19.993
  CI (99.9%): [213.215, 213.823] (assumes normal distribution)
  Samples, N = 46798
        mean =    213.519 ±(99.9%) 0.304 us/op
         min =    206.848 us/op
  p( 0.0000) =    206.848 us/op
  p(50.0000) =    207.360 us/op
  p(90.0000) =    220.672 us/op
  p(95.0000) =    229.888 us/op
  p(99.0000) =    323.072 us/op
  p(99.9000) =    437.760 us/op
  p(99.9900) =    479.888 us/op
  p(99.9990) =    565.248 us/op
  p(99.9999) =    565.248 us/op
         max =    565.248 us/op


Iteration   1: n = 5174, mean = 193 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 188, 188, 200, 207, 261, 382, 426, 426 us/op
Iteration   2: n = 5205, mean = 192 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 188, 188, 197, 203, 260, 398, 419, 419 us/op
Iteration   3: n = 5196, mean = 192 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 188, 188, 197, 203, 262, 392, 408, 408 us/op
Iteration   4: n = 5180, mean = 193 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 188, 188, 201, 207, 268, 381, 398, 398 us/op
Iteration   5: n = 5196, mean = 192 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 188, 188, 197, 206, 259, 399, 514, 514 us/op
Iteration   6: n = 5183, mean = 193 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 188, 188, 199, 206, 263, 389, 414, 414 us/op
Iteration   7: n = 5169, mean = 193 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 188, 188, 201, 207, 261, 390, 429, 429 us/op
Iteration   8: n = 5185, mean = 193 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 188, 188, 199, 207, 252, 395, 426, 426 us/op
Iteration   9: n = 5186, mean = 193 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 188, 188, 199, 206, 253, 360, 392, 392 us/op
Iteration  10: n = 5177, mean = 193 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 188, 188, 200, 207, 257, 398, 435, 435 us/op

Result "testHeapByteBufferLE":
  192.723 ±(99.9%) 0.216 us/op [Average]
  (min, avg, max) = (187.904, 192.723, 513.536), stdev = 14.930
  CI (99.9%): [192.508, 192.939] (assumes normal distribution)
  Samples, N = 51851
        mean =    192.723 ±(99.9%) 0.216 us/op
         min =    187.904 us/op
  p( 0.0000) =    187.904 us/op
  p(50.0000) =    188.416 us/op
  p(90.0000) =    198.656 us/op
  p(95.0000) =    206.592 us/op
  p(99.0000) =    259.328 us/op
  p(99.9000) =    389.632 us/op
  p(99.9900) =    424.656 us/op
  p(99.9990) =    513.536 us/op
  p(99.9999) =    513.536 us/op
         max =    513.536 us/op


Iteration   1: n = 11004, mean = 45 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 44, 44, 45, 47, 70, 98, 107, 107 us/op
Iteration   2: n = 11028, mean = 45 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 44, 44, 45, 47, 69, 106, 146, 148 us/op
Iteration   3: n = 11026, mean = 45 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 44, 44, 46, 47, 65, 105, 114, 115 us/op
Iteration   4: n = 11000, mean = 45 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 44, 44, 45, 47, 71, 106, 114, 115 us/op
Iteration   5: n = 11001, mean = 45 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 44, 44, 45, 47, 72, 106, 156, 158 us/op
Iteration   6: n = 11084, mean = 45 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 44, 44, 45, 47, 63, 106, 114, 115 us/op
Iteration   7: n = 10998, mean = 45 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 44, 44, 46, 47, 71, 100, 131, 133 us/op
Iteration   8: n = 11009, mean = 45 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 44, 44, 45, 47, 71, 105, 106, 106 us/op
Iteration   9: n = 10933, mean = 46 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 44, 44, 47, 47, 73, 106, 117, 117 us/op
Iteration  10: n = 10578, mean = 47 us/op, p{0.00, 0.50, 0.90, 0.95, 0.99, 0.999, 0.9999, 1.00} = 44, 45, 47, 63, 88, 116, 145, 146 us/op

Result "testUnsafeLE":
  45.545 ±(99.9%) 0.053 us/op [Average]
  (min, avg, max) = (43.968, 45.545, 157.696), stdev = 5.347
  CI (99.9%): [45.492, 45.598] (assumes normal distribution)
  Samples, N = 109661
        mean =     45.545 ±(99.9%) 0.053 us/op
         min =     43.968 us/op
  p( 0.0000) =     43.968 us/op
  p(50.0000) =     44.224 us/op
  p(90.0000) =     46.592 us/op
  p(95.0000) =     46.912 us/op
  p(99.0000) =     72.064 us/op
  p(99.9000) =    105.856 us/op
  p(99.9900) =    124.071 us/op
  p(99.9990) =    156.756 us/op
  p(99.9999) =    157.696 us/op
         max =    157.696 us/op


# Run complete. Total time: 00:03:02

Benchmark                                       Mode     Cnt    Score   Error  Units
BufferAccessBenchmark.testDirectByteBufferBE  sample  143538   69.583 ± 0.088  us/op
BufferAccessBenchmark.testDirectByteBufferLE  sample  108502   46.035 ± 0.064  us/op
BufferAccessBenchmark.testHeapByteArray       sample   64418  155.058 ± 0.194  us/op
BufferAccessBenchmark.testHeapByteBufferBE    sample   46798  213.519 ± 0.304  us/op
BufferAccessBenchmark.testHeapByteBufferLE    sample   51851  192.723 ± 0.216  us/op
BufferAccessBenchmark.testUnsafeLE            sample  109661   45.545 ± 0.053  us/op

結果

int型の場合

まず、(驚いたことに)DirectBufferでのint型(32bit)のLittleEndianのデコードが、かなり速い



そして、Unsafeが、当然速いというのは分かったが、DirectByteBuffer(Little Endian)も、Unsafeとかわらないぐらい速い。
これはバッファサイズが512kibの場合だが、16kibなど小さいバッファで比較しても同様である。

(このくらい速ければ、わざわざ非推奨なベンダ依存のAPIを使う必要もなさそうである。)


正直、この速さは意外だった。
しかし、どうして速いのかは、よく分からない。


CPUのネイティブなレイアウトであるLittle Endianと同じだから変換不要で速くなるとかかな?と思ってみたものの、同じDirectByteBufferのBigEndianのケースでも、byteをラップしたヒープによるByteBufferよりも速いので、そうゆう問題ではないのかもしれない。

byteをラップしたByteBufferに問題があるのかと思って、自前でbyte[]をLittle Endianのintとしてアクセスするベンチマークとも比較してみたが、似たような性能となったので、特別にByteBufferの仕組みに問題があるわけではなさそう。


どうやら、StackOverflowで見つけた記事によると、ヒープのByteBufferとダイレクトのByteBufferでは、以下のように読み取り方法が違うらしい。
http://stackoverflow.com/questions/6943858/bytebuffer-getint-question

// HeapByteBuffer

public int getInt() {
    return Bits.getInt(this, ix(nextGetIndex(4)), bigEndian);
}

public int getInt(int i) {
    return Bits.getInt(this, ix(checkIndex(i, 4)), bigEndian);
}

// DirectByteBuffer

private int getInt(long a) {
    if (unaligned) {
        int x = unsafe.getInt(a);
        return (nativeByteOrder ? x : Bits.swap(x));
    }
    return Bits.getInt(a, bigEndian);
}

public int getInt() {
    return getInt(ix(nextGetIndex((1 << 2))));
}

public int getInt(int i) {
    return getInt(ix(checkIndex(i, (1 << 2))));
}

これから考えると、

  1. 単純にUnsafe#getIntで読み取れれば最速である。
  2. DirectBufferでは、もし、エンディアンが一致しない場合には、後続にswap処理が入る分、遅くなる。

これがDirectBufferでのintアクセス時のリトルエンディアンとビッグエンディアンとのパフォーマンスの違いのようだ。

Double型の場合

ついでに、Double(8バイト)型の場合も計測してみた。

Benchmark                                       Mode  Cnt      Score     Error  Units
BufferAccessBenchmark2.testDirectByteBufferBE  thrpt   10  18947.259 ± 179.970  ops/s
BufferAccessBenchmark2.testDirectByteBufferLE  thrpt   10  19048.996 ±  50.217  ops/s
BufferAccessBenchmark2.testHeapByteBufferBE    thrpt   10   3955.089 ±   6.115  ops/s
BufferAccessBenchmark2.testHeapByteBufferLE    thrpt   10   3869.472 ±  57.203  ops/s
BufferAccessBenchmark2.testUnsafeLE            thrpt   10  18790.156 ±  24.251  ops/s


BigEndianとLittleEndianとでのパフォーマンスの違いが見られなくなっているが、UnsafeとDirectBufferのパフォーマンスが同等であることはintの場合と同じようである。(BE/LEの違いが見られない理由はわからないのだが...)


バッファサイズは512kibのままであり、double型は8バイト表現となるので、データの読み取り回数としてはintの場合に対して半分になるが、パフォーマンスとしてはダイレクトバッファを使うことにより、intの場合と遜色ない高速な読み取りができるように見える。


ヒープ上のバッファからのdouble読み取りは、やはり遅いようだ。

Float型の場合

intと同様に4バイト表現であるfloatでは、以下のような結果になった。

Benchmark                                       Mode  Cnt     Score    Error  Units
BufferAccessBenchmark2.testDirectByteBufferBE  thrpt   10  9538.018 ± 45.020  ops/s
BufferAccessBenchmark2.testDirectByteBufferLE  thrpt   10  9528.538 ± 25.403  ops/s
BufferAccessBenchmark2.testHeapByteBufferBE    thrpt   10  4981.542 ± 85.246  ops/s
BufferAccessBenchmark2.testHeapByteBufferLE    thrpt   10  5059.595 ± 74.801  ops/s
BufferAccessBenchmark2.testUnsafeLE            thrpt   10  9533.223 ± 22.078  ops/s

BigEndianとLittleEndianとでのパフォーマンスの違いが見られなくなっているが、UnsafeとDirectBufferのパフォーマンスが同等である傾向は同じようである。

やはり、double同様に、ヒープからfloatを取得するよりも、ダイレクトバッファから直接floatを読み取ったほうが速い。


ただし、doubleと比較すると、ダイレクトバッファの場合は、floatのほうがパフォーマンスが低いように見える。

これはJavaでは内部的には実数がdouble型として扱われているところに起因するのだろうか?(根拠はないが...。)

(1つの推測としては、バッファサイズよりも、項目数 = get回数が効いているのかもしれない。)


※ ヒープ同士の比較では、doubleよりはfloatのほうがパフォーマンスは良いようである。

Long型の場合

long値(8バイト)の場合は、以下のようになった。

Benchmark                                       Mode  Cnt      Score      Error  Units
BufferAccessBenchmark2.testDirectByteBufferBE  thrpt   10  29425.796 ±  659.844  ops/s
BufferAccessBenchmark2.testDirectByteBufferLE  thrpt   10  36945.695 ± 1012.917  ops/s
BufferAccessBenchmark2.testHeapByteBufferBE    thrpt   10   3924.688 ±   66.375  ops/s
BufferAccessBenchmark2.testHeapByteBufferLE    thrpt   10   3929.151 ±   57.227  ops/s
BufferAccessBenchmark2.testUnsafeLE            thrpt   10  38615.889 ±  186.971  ops/s


(intよりもパフォーマンスが高くなったのでグラフの横幅が変わっていることに注意。)


long型は8バイトなのでdouble型のケースと同様に、同じ512kibバッファサイズではintやfloatと比較して半分の項目数しかない。


intのケースよりも一秒間当たりの実行回数が倍近く伸びているので、やはり、Unsafe#getX() メソッドの呼び出し回数がパフォーマンスの大きな因子のような感じである。

結論

これらの結果をみるかぎり、整数、実数ともに連続的に大量の項目を読みとる必要がある場合は、ダイレクトバッファを使うのが良さそうである。
(というより、このグラフを見てしまうとダイレクトバッファを使わないのは、もはや間違いといえるレベルかも...。)


とりあえず、ファイル等から直接データを読み取る場合には、一旦バイト配列としてヒープにもってきてからint等に展開するよりも*4直接、ダイレクトバッファ上からint値などを取得したほうが速くなりそうな結果が得られたように思える。

(ただし、本当にアテにしていいのかは、実際にコードを書いて比較するべきだろう。)


この結果は、個人的には、なかなか興味深い、今まで気にも留めなかったダイレクトバッファの正しい使い道が分かったような新鮮な発見があり、
今後のI/Oまわりのパフォーマンスチューニングに使えそうな、とても有意義な勉強になったと思う。


※ 今回のソースコードは、以下のgistに置いた。
(pom.xmlと、ベンチマークjavaクラスのみ。それ以外のソースはmavenで自動生成されたものなので手は入れていない。追試する場合は、上記手順に従ってmavenからjmhのアーキタイプを指定してプロジェクトを構築してから、本ソースを追加すれば良い。)
https://gist.github.com/seraphy/6276dbae1eb4ee96325d


以上、メモ終了。

*1:便利だと思うので、最近は、だいたいPleiades (Eclipse 4.5 Mars)を使っている

*2:こちらは、jdk9でUnsafeから公開APIになるかもしれない? http://openjdk.java.net/jeps/193 http://openjdk.java.net/jeps/260

*3:http://www.docjar.com/docs/api/sun/misc/Unsafe.html#objectFieldOffset

*4:FileChannelからByteBuffer/MappedByteBufferなどを使ってダイレクトバッファを取得できる。ただし、MappedByteBufferは最速らしいが、unmapする方法が現状ではgcしかないので、開きっぱなしにするファイルでないと使いどころは難しいかも?