Hatena::ブログ(Diary)

seraphyの日記 このページをアンテナに追加 RSSフィード

2015-11-20 DirectByteBufferの注意点とパフォーマンス比較

Javaのダイレクトバッファの利用上の注意点と、各種バッファを使った読み込みパフォーマンスの比較  Javaのダイレクトバッファの利用上の注意点と、各種バッファを使った読み込みパフォーマンスの比較を含むブックマーク

概要

前回では、ヒープ上に確保したByteBufferと、ダイレクトバッファとして作成したByteBufferでのデータの読み取りパフォーマンスを比較した結果、圧倒的にダイレクトバッファが速いことが分かった。


しかし上記ベンチマークは、あえてByteBuffer単体でのパフォーマンスの比較を行ったものであり、実際のファイル入出力処理を含んだ比較ではない。

いわば、純粋なByteBufferの性能比較である。


では、実際にファイルの読み取りを含んだ場合でも、はたしてダイレクトバッファを使ったほうが速いのであろうか?


今回は、実際の利用方法を意識して、このあたりを調べてみることとする。


ヒープバッファとダイレクトバッファの違い

ByteBufferには大きく分けて2種類がある。

  • ヒープ上の作成されるヒープバッファ
  • ネイティブ領域に作成されるダイレクトバッファ

ヒープバッファは以下のように作成される。

ByteBuffer buf = ByteBuffer.alloc(siz);
// または
ByteBuffer buf = ByteBuffer.wrap(new byte[siz]);

(allocは単純に内部でnew byte[]を実行しているだけである。)

これは、当然、Javaのヒープ領域からバッファが割り当てられる。

バッファとして確保した分、利用可能なヒープサイズは減ることになる。

その特性はJavaの一般的なオブジェクトと何ら変わることは無い。


これに対して、ダイレクトバッファは以下のように作成される。

    ByteBuffer buf = ByteBuffer.allocDirect(siz);

(そのほか、RandomAccessFileから直接MappedByteBufferを取得する方法もある。)


こちらはJavaのヒープではなく、ネイティブメモリ上から割り当てられる。


ダイレクトバッファはヒープ上にバッファは作らないので、ヒープはほとんど減らない。

そのかわり、ネイティブメモリ、つまりOS側からJavaVMが借りているコミットメモリが増える。

ネイティブメモリはガベージコレクタが管理しているメモリの範囲外であるため、ガベージコレクトなどのメカニズムが効かない領域である。


実験コード
public class BufferAllocExample {

    /**
     * バイトバッファをallocする方法
     */
    public enum ByteBufferAllocStrategy {

        /**
         * ヒープのByteBufferを構築する.
         */
        heap() {
            @Override
            public ByteBuffer alloc(int siz) {
                return ByteBuffer.allocate(siz);
            }
        },

        /**
         * ダイレクトのByteBufferを構築する.
         */
        direct() {
            @Override
            public ByteBuffer alloc(int siz) {
                return ByteBuffer.allocateDirect(siz);
            }
        };

        public abstract ByteBuffer alloc(int siz);
    }

    /**
     * エントリポイント.
     * 第一引数にheapかdirectを指定する.
     * @param args
     * @throws Exception
     */
    public static void main(String... args) throws Exception {
        ByteBufferAllocStrategy allocator = ByteBufferAllocStrategy
                .valueOf(args[0]);

        Runtime rt = Runtime.getRuntime();

        List<ByteBuffer> buffers = new ArrayList<>();

        // 10MiBの割り当てを10回繰り返す
        for (int idx = 0; idx < 10; idx++) {
            System.gc();
            System.out.println(rt.freeMemory() + "/" + rt.totalMemory());

            ByteBuffer buf = allocator.alloc(1024 * 1024 * 10);
            buffers.add(buf);
        }

        // 100MiB割り当て後のヒープの残量
        System.gc();
        System.out.println(rt.freeMemory() + "/" + rt.totalMemory());

        // OSのコミットサイズ、プライベートワーキングセットのサイズを
        // タスクマネージャで見られるように、アプリ終了前に一旦停止する。
        System.console().readLine();
    }
}

第一引数にallocかallocDirectを指定して実行すると、10MiBごとのバッファを確保しながらヒープの残量を表示し、最後に入力待ちになって停止するアプリである。

ヒープバッファ確保の場合
>java -Xms128m -Xmx128m -cp classes jp.seraphyware.jmhexample.BufferAllocExample heap
127831384/128974848
118026720/128974848
107540896/128974848
97055072/128974848
86569248/128974848
76083424/128974848
65597600/128974848
55111776/128974848
44625952/128974848
34140128/128974848
23654304/128974848 

バッファを確保するたびにヒープがどんどん減ってゆく順当な動きである。


このときのタスクマネージャで取得したOSから予約されたコミットサイズは、

  • コミットサイズ = 184,444k

となっていた。

ダイレクトバッファの場合
>java -Xms128m -Xmx128m -cp classes jp.seraphyware.jmhexample.BufferAllocExample direct
127831360/128974848
128512256/128974848
128512120/128974848
128511984/128974848
128511848/128974848
128511712/128974848
128511576/128974848
128511440/128974848
128511304/128974848
128511168/128974848
128511032/128974848

ヒープはぜんぜん減っていない。

このときのタスクマネージャで取得したOSから予約されたコミットサイズは、

  • コミットサイズ = 286,676k

となっている。


ヒープバッファのときと比較して、コミットサイズ = OSからJavaVMが予約しているメモリとしては確実に100MB(102,232k)ほど増えている。


ダイレクトバッファの最大サイズの指定方法

ちなみに、ダイレクトバッファの最大値はJVMのオプション"-XX:MaxDirectMemorySize "のように明示的に設定することができる。


省略した場合は自動(≒ 制限なし)で、既定ではヒープの最大値と同じようになるため、通常は気にしなくてもよいらしい。

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html


ただし、最大ヒープサイズを超えてダイレクトバッファサイズを指定することもできるらしく、以下のように最大ヒープサイズを64MBにしても、それを超える100MBのダイレクトバッファを確保することが可能であった。(Java8u60で確認。)

>java -Xms64m -Xmx64m -XX:MaxDirectMemorySize=512m -cp classes jp.seraphyware.jmhexample.BufferAllocExample direct
63689944/64487424
64020424/64487424
63684664/64487424
63684528/64487424
63684392/64487424
63684256/64487424
63684120/64487424
63683984/64487424
63683848/64487424
63683712/64487424
63683656/64487424

バッファ確保のパフォーマンス比較

ヒープ上のByteBufferは、単に普通にヒープ上でnew byte[…]するだけのオーバーヘッドと同等である。

ヒープメモリは事前にJavaVMがOSよりメモリをもらっており、そこでJavaVMが領域を割り当てるだけである。


これに対して、ダイレクトバッファはネイティブ領域に都度、OSよりバッファを確保してもらっているような動きになっている。

(実際は、もう少し効率は良いかもしれないが、前述の実験からすると、要求されてから都度、OSからメモリをもらっている感じである。)


両者はメモリの確保方法が異なるため、確保するまでにかかる処理時間も異なると予想される。

ベンチマーク

そこで、以下のようなベンチマークコードを書いて計測してみた。

public class BufferAllocBenchmark {

    @State(Scope.Thread)
    public static class BufferAllocContext {

        private ByteBuffer buf;

        @Setup(Level.Trial)
        public void setup() {
        }

        public void setByteBuffer(ByteBuffer buf) {
            this.buf = buf;
        }

        public ByteBuffer getButeBuffer() {
            return this.buf;
        }
    }

    @Benchmark
    public void testAllocDirect8(BufferAllocContext ctx) {
        ctx.setByteBuffer(ByteBuffer.allocateDirect(8 * 1024));
    }

    @Benchmark
    public void testAllocHeap8(BufferAllocContext ctx) {
        ctx.setByteBuffer(ByteBuffer.allocate(8 * 1024));
    }
  … (中略) …
}

ByteBuffer.alloc()と、ByteBuffer.allocDirect()をバッファサイズを変えて計測している。

計測するバッファサイズは、8, 16, 32, 64, 128, 256, 512, 1024kibである。


(過度な最適化が起きないように、得られたByteBufferは状態インスタンスに1世代のみ保存している。)


Javaはjdk1.8u60で実行し、オプションとして-Xmx128m -Xmx128m -XX:MaxDirectMemorySize=256mを指定したものである。


計測結果
sizeDirectHeapDirect ErrorHeap Error
8183090.2162018841.709320.86349630.037
1693116.2451096969.848191.7548257.477
3247854.178547637.30282.98613006.356
6422689.129266632.138224.025163.652
12810988.46569412.152518.684864.918
2565536.21533852.752223.10884.935
5122897.21928651.80324.236406.872
10241574.25414013.50651.023104.658

数値は、1秒あたりのベンチマークメソッドの実行回数の平均である。

激しくガベージコレクタが動作することが予想されるので、gcによる揺らぎを考慮して測定回数(イテレーション)は50回まわすことにした。


これをチャート化すると、以下のようになる。

チャート化するにあたり、数値は実行回数ではなく確保したバイト数に換算している。

(= 実行した回数 x 確保したバイト数)


これにより、バッファ確保の効率を比較できるようにしている。

f:id:seraphy:20151121024641p:image

これを見ると、明らかにダイレクトバッファはヒープバッファよりも時間がかかっている。


差が10倍ほど開いてしまったので、縦軸は対数表示している。

ダイレクトバッファは、確保するまでの時間が、むちゃくちゃ遅い。


この点については、そもそもJavaDocに書かれている。

ダイレクト byte バッファーは、このクラスのファクトリメソッド allocateDirect を呼び出すと作成されます。通常は、こちらのバッファーのほうが、非ダイレクトバッファーよりも割り当ておよび解放コストがやや高くなります。ダイレクトバッファーの内容が標準のガベージコレクトされたヒープの外部にあるなら、アプリケーションのメモリーフットプリントに対する影響はわずかです。このことから、ダイレクトバッファーには、基本となるシステム固有の入出力操作に従属する、寿命が長く容量の大きいバッファーを指定することをお勧めします。一般に、ダイレクトバッファーの割り当ては、プログラムの性能を十分に改善できる見込みがある場合にのみ行うべきです。

http://docs.oracle.com/javase/jp/8/docs/api/java/nio/ByteBuffer.html


チャートを見ると揺らぎがあるが、だいたいメモリ確保のスピードは確保するバッファサイズに係わらず、ほぼ一定のように思われるので、バッファが小さければ、あるいは、大きければバッファ確保が速くなるとか、そうゆうことはなさそうである。


いずれにしても、ヒープとのダイレクトバッファの確保時のパフォーマンスの差はかなり大きい。


DirerctByteBufferの連続確保と破棄の問題点

ヒープ上のbyte[]配列であるヒープバッファは連続確保・破棄したとしても、その動きは通常のJavaオブジェクトと変わらないことが予想される。

また、OSから見た使用メモリには増減は発生しないはずである。

(すでに確保済みのヒープ内で処理されるため。)


これに対して、ダイレクトバッファはOSより都度メモリを借り、そのメモリを解放するためには、そのダイレクトバッファのハンドルをもつヒープ上のByteBufferオブジェクトがgcされるまで待たなければならないはずである。

(ByteBufferのメソッドにはI/O処理につきものの「close」のようなメソッドがないため、バッファが使用中であるかどうかを判断するにはgcによるしかない。)

また、OSから見た使用メモリは、ダイレクトバッファは確保されるたびにコミットサイズが増加し、gcが発生するたびに解放されるような動きを示すはずである。

実験

そこで、以下のようなコードを書いて試してみた。

public class BufferStressExample2 {

    /**
     * 最新のバイトバッファを保持するスレッドローカル
     */
    public static ThreadLocal<ByteBuffer> lastBufTLS = new ThreadLocal<>();;

    /**
     * バイトバッファをallocする方法
     */
    public enum ByteBufferAllocStrategy {

        /**
         * ヒープのByteBufferを構築する.
         */
        heap() {
            @Override
            public ByteBuffer alloc(int siz) {
                return ByteBuffer.allocate(siz);
            }
        },

        /**
         * ダイレクトのByteBufferを構築する.
         */
        direct() {
            @Override
            public ByteBuffer alloc(int siz) {
                return ByteBuffer.allocateDirect(siz);
            }
        };

        public abstract ByteBuffer alloc(int siz);
    }

    /**
     * バイトバッファの破棄方法.
     */
    public enum ByteBufferDeallocStrategy {

        /**
         * 何もしない.システムにお任せ.
         */
        none() {
            @Override
            public void dealloc(ByteBuffer buf) {
            }
        },

        /**
         * 明示的にgcを呼び出す.
         */
        gc() {
            @Override
            public void dealloc(ByteBuffer buf) {
                System.gc();
            }
        },

        /**
         * 明示的にcleanerを呼び出す.
         */
        cleaner() {
            @Override
            public void dealloc(ByteBuffer buf) {
                if (buf != null && buf.isDirect()) {
                    destroyDirectByteBuffer(buf);
                }
            }
        };

        public abstract void dealloc(ByteBuffer buf);
    }

    /**
     * エントリポイント
     * @param args
     * @throws Exception
     */
    public static void main(String... args) throws Exception {

        ByteBufferAllocStrategy allocator = ByteBufferAllocStrategy.valueOf(args[0]);
        ByteBufferDeallocStrategy deallocator;
        if (args.length > 1) {
            deallocator = ByteBufferDeallocStrategy.valueOf(args[1]);
        } else {
            deallocator = ByteBufferDeallocStrategy.none;
        }

        int mxthread = (args.length > 2) ? Integer.parseInt(args[2]) : 2;

        // 10MiBの割り当てを永久に繰り返すジョブ
        Runnable job = () -> {
            for (;;) {
                try {
                    // ByteBufferの構築
                    ByteBuffer buf = allocator.alloc(10 * 1024 * 1024); // 10MiB

                    // ByteBufferの破棄
                    deallocator.dealloc(lastBufTLS.get());

                    // 最後に確保したByteBufferの保存
                    lastBufTLS.set(buf);

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

        IntFunction<Thread> makeThread = (idx) -> {
            Thread t = new Thread(job);
            t.setDaemon(false);
            t.setName("bufferStress:" + idx);
            return t;
        };

        IntStream.range(0, mxthread)
                .mapToObj(makeThread)
                .forEach(Thread::start);
    }
}

JDK1.8u60で実行した。

テストコードでは2スレッドの同時バッファ確保を行っている。

またコマンドは以下のオプションで実行した。

java -cp classes -Xms128m -Xmx128m -XX:MaxDirectMemorySize=256m jp.seraphyware.jmhexample.BufferStressExample2 heap
ヒープによるByteBufferの連続確保と破棄

以下はWindowsのパフォーマンスモニタによるPage File Bytes(≒コミットサイズ)と、ワーキングセットサイズの推移グラフである。

(縦軸の単位は10MiBである。以下のパフォーマンスモニタのチャートはすべて同様である。)

f:id:seraphy:20151121025007p:image

new byte[]の破棄と解放はJavaVMの中の出来事であり、OSから見てメモリ消費の増減はないのが分かる。


ダイレクトバッファによるByteBufferの連続確保と破棄と、OutOfMemory問題

f:id:seraphy:20151121025008p:image

このテストコードでは、ダイレクトバッファは1回につき10Mbytesを割り当て、直近の1つのByteBufferだけを保存し、それ以前のByteBufferは参照がなくなりガベージコレクト対象としている。

これが2スレッドで動いているので、理想的にはダイレクトバッファは1世代前の10Mbytes x 2と、作成されたばかりの10Mbytes x 2の、計40Mbytesのバッファ以外は不要のはずである。


しかし、パフォーマンスモニタによると、瞬間的には450Mbytesほどのコミットサイズになっている。(ヒープサイズが128MB、これに加えてMaxDirectMemorySizeが256MBなので、384MB+αぐらいが最大値になっていると思われる。)

つまり、不要になったネイティブメモリは、すぐには消えていない。


また、このテストでは、MaxDirectMemorySizeで十分なサイズを指定しておかないと、「OutOfMemoryError: Direct buffer memory」とメモリ不足エラーが発生しやすくなる。

また、スレッド数を増やすと、如実にOutOfMemoryErrorが発生するようになる。

実際には10Mbytesしか要求しておらず、最大バッファサイズが256MBもあり、十分に空きがあると考えられるにも係わらず、である。

おそらく確保するスピードが解放するスピードを上回っているのだろう。


ダイレクトバッファでOutOfMemoryを避ける方法

原理的には、ByteBufferがgcされるまでネイティブに確保されたダイレクトバッファも解放されないであろうため、最大ダイレクトメモリサイズよりヒープが大きい場合は、場合によってはヒープのgcが発生する前にダイレクトメモリが枯渇する可能性があるように思われる。(まさか、そんなナイーブな実装ではないとは思うし、OutOfMemoryをあげる前にgcは試行されるはずだが…。)

現実に、Windows7/8.1上のjdk1.8u60では、MaxDirectMemorySizeが十分に大きくないと、このテストでは、すぐにOutOfMemoryが発生する状況である。


System.gc()によるOutOfMemoryの回避

ダイレクトバッファを確保する直前にSystem.gc()をしてあげると、この事象はでてこない。

ByteBufferがgcされ、ネイティブの解放も逐次行われて、パフォーマンスモニタの波形も比較的穏やかなものになる。

f:id:seraphy:20151121025009p:image

ただし、はたして実運用コードの中に「System.gc()」のようなコードを書いて良いものか?

(あるいは、-XX:+DisableExplicitGC オプションによりgcが無視される可能性もある。)

、というような課題はある。


DirectBuffer内部のcleanerを直接使用した早期解放を使う場合

実は、どうやらダイレクトバッファの破棄が遅延されてOutOfMemoryErrorが発生する問題は、かなりメジャーな問題のようで、Stackoverflowあたりを検索すると沢山出てくる。


System.gc()を使うのが、おそらくもっとも正式な方法だが、DirectByteBufferが内部で持っているcleanerメソッドを呼び出して、それに対してcleanを実行することで、ネイティブのメモリ破棄処理を強制的に早期に実行させる、という方法もあるようである。


以下に引用する、ApacheのHadoopのソースにも、ダイレクトバッファを破棄するためのコードが書かれている。

(および、どうしてダイレクトバッファの破棄が遅延するのかの理由も。)

https://hbase.apache.org/0.94/apidocs/src-html/org/apache/hadoop/hbase/util/DirectMemoryUtils.html#line.80

    /**
      * DirectByteBuffers are garbage collected by using a phantom reference and a
      * reference queue. Every once a while, the JVM checks the reference queue and
      * cleans the DirectByteBuffers. However, as this doesn't happen
      * immediately after discarding all references to a DirectByteBuffer, it's
      * easy to OutOfMemoryError yourself using DirectByteBuffers. This function
      * explicitly calls the Cleaner method of a DirectByteBuffer.
      * 
      * @param toBeDestroyed
      *          The DirectByteBuffer that will be "cleaned". Utilizes reflection.
      *          
      */
    public static void destroyDirectByteBuffer(ByteBuffer toBeDestroyed)
        throws IllegalArgumentException, IllegalAccessException,
        InvocationTargetException, SecurityException, NoSuchMethodException {
    
      Preconditions.checkArgument(toBeDestroyed.isDirect(),
          "toBeDestroyed isn't direct!");
    
      Method cleanerMethod = toBeDestroyed.getClass().getMethod("cleaner");
      cleanerMethod.setAccessible(true);
      Object cleaner = cleanerMethod.invoke(toBeDestroyed);
      Method cleanMethod = cleaner.getClass().getMethod("clean");
      cleanMethod.setAccessible(true);
      cleanMethod.invoke(cleaner);
    
    }

このコメントによると、参照がすべて切れたからといって、ただちにネイティブメモリが解放されるわけではなく、ダイレクトバッファはファントムリファレンスによって保持され、そのリファレンスキューをJavaVMがチェックして、gcによりキューに入れられたバッファがあれば、そのネイティブメモリを解放しているようである。

このコメントから察するに、タイミングは不明だが、ともかくリファレンスキューを見に行く必要のあるパッシブな動きになっているのだろう。


ためしに、このcleanerを呼び出すコードを使ってみると、パフォーマンスモニタの波形は、大変穏やかなものとなり、スレッド数を増やしてもOutOfMemoryも発生しなくなる。

f:id:seraphy:20151121025010p:image


ただし、はたして実運用コードの中にJavaVMの特定の実装にべったり依存するかのようなコードを書いてよいものかどうか、そのあたりの課題は残る。

ヒープバッファとダイレクトバッファのファイルの読み込み速度の比較

以上の考察から、ダイレクトバッファを使うには2つ注意点があることが分かった。

  1. ダイレクトバッファは確保するのが遅い。
  2. ダイレクトバッファは破棄するのが難しい。

つまり、ファイルを開くたびに、ダイレクトバッファを毎回確保するような方法では、バッファ確保時のオーバーヘッドと、バッファ破棄のトラブルによって、あまり良いパフォーマンスは得られない可能性がある。


しかし、「単にファイルをシーケンシャルに読み込みたい」という、ありふれたニーズであれば、これを対策するのは比較的容易である。


単に、ダイレクトバッファは確保したらキャッシュし、破棄しなければ良いだけである。

(同時アクセスするファイルの最大数分だけダイレクトバッファをキャッシュすれば良い。)

*1


これを実装した上で、ダイレクトバッファとヒープバッファでのファイル読み取りのパフォーマンスを比較してみたいと思う。

実験内容
  • テストデータはテンポラリ上に作成される10Mbytesのファイルである。
  • テストデータはLittle EndianとBig Endianの2種類作成する
  • データはIntとDoubleの2種類作成する。
  • バッファサイズは、8, 16, 32, 64, 128, 256, 512, 1024KiB
  • 読み取り方式として、
    • ダイレクトバッファ (バッファは@Setup時に予め初期化しておき、各イテレーションでは確保しない)
    • ヒープバッファ (バッファは@Setup時に予め初期化しておき、各イテレーションでは確保しない)
    • BufferedInputStream
    • InputStream (バッファ忘れケース)
  • ウォームアップとして10回、計測として10回のイテレーションを行う

なお、ウォームアップを10回も繰り返すと、10MBytes程度のファイルならOSのファイルキャッシュに確実に乗っかることになる。

しかし、今回はディスクの性能を比較するわけではないので、これで良いものと考える。

ある意味、理想的なディスクを使った場合の読み取り方式の違いによるパフォーマンスの違いを計測するようなものといえるだろう。

データ作成コード

(Intデータ部のみ。Doubleデータのソースコードの記載は省略)

データ共通部

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

    protected final ByteOrder byteOrder;

    protected final int bufsiz;

    private Path tempFile;

    private ByteBuffer bufHeap;

    private ByteBuffer bufDirect;

    protected AbstractFileBenchContext(ByteOrder byteOrder, int bufsiz) {
        this.byteOrder = byteOrder;
        this.bufsiz = bufsiz;
    }

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

        // バッファの確保 (ヒープ)
        byte[] data = new byte[bufsiz];
        bufHeap = ByteBuffer.wrap(data);
        bufHeap.order(byteOrder);

        // バッファの確保 (ダイレクト)
        bufDirect = ByteBuffer.allocateDirect(data.length);
        bufDirect.order(byteOrder);

        // テストデータファイルの作成
        tempFile = initTempFile();
    }

    protected abstract Path initTempFile() throws IOException;

    /**
     * スレッドごとベンチマーク終了ごとに呼び出される.
     */
    @TearDown
    public void teardown() {
        try {
            //System.out.println("★delete tempFile=" + tempFile);
            Files.deleteIfExists(tempFile);

        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    public Path getTempFile() {
        return tempFile;
    }

    public abstract void verify(Object actual);

    public ByteBuffer getBufDirect() {
        return bufDirect;
    }

    public ByteBuffer getBufHeap() {
        return bufHeap;
    }
}

テストごとのデータ部(一部)

    public static class BenchContextBase extends AbstractFileBenchContext {

        public static final int reqSize = 1024 * 1024 * 10; // 10MiBytes

        private long total;

        protected BenchContextBase(ByteOrder byteOrder, int bufsiz) {
            super(byteOrder, bufsiz);
        }

        /**
         * テストデータファイルの作成
         * @throws IOException
         */
        @Override
        protected Path initTempFile() throws IOException {
            Path tempFile = Files.createTempFile("filebufferbench", ".tmp");
            //System.out.println("★create tempFile=" + tempFile);

            ByteBuffer buf = ByteBuffer.allocate(bufsiz);
            buf.order(byteOrder);

            total = 0;
            int capacity = buf.capacity();
            long siz;
            try (FileChannel channel = (FileChannel) Files
                    .newByteChannel(tempFile, CREATE, TRUNCATE_EXISTING, WRITE)) {
                int idx = 0;
                int mx = reqSize / capacity;
                for (int loop = 0; loop < mx; loop++) {
                    buf.clear();
                    while (buf.position() < capacity) {
                        buf.putInt(idx);
                        total += idx;
                        idx += 1;
                    }
                    buf.flip();
                    channel.write(buf);
                }
                siz = channel.size();
            }

            if (reqSize != siz) {
                throw new RuntimeException();
            }
            //System.out.println("★total=" + total + "/size=" + siz);
            return tempFile;
        }

        @Override
        public void verify(Object actual) {
            if ((Long) actual != total) {
                throw new RuntimeException("actual=" + actual);
            }
         }
    }

    public static class BenchContextLE8k extends BenchContextBase {

        public BenchContextLE8k() {
            super(ByteOrder.LITTLE_ENDIAN, 1024 * 8);
        }
    }

    public static class BenchContextBE8k extends BenchContextBase {

        public BenchContextBE8k() {
            super(ByteOrder.BIG_ENDIAN, 1024 * 8);
        }
    }

※ このあと、バッファサイズ、LE/BEごとのコンテキストクラスが延々と続く。

テスト実行部

FileChannelを使ってByteBufferでファイルの中身を取り出しテストする部分。

    /**
     * バイトバッファからint値を読み出すテスト
     * @param buf
     */
    private static long runByteBuffer(FileChannel channel, ByteBuffer buf)
            throws IOException {
        long total = 0;
        for (;;) {
            buf.clear();
            int len = channel.read(buf);
            if (len < 0) {
                break;
            }
            buf.flip();
            int limit = buf.limit();
            while (buf.position() < limit) {
                total += buf.getInt();
            }
        }
        return total;
    }

データファイルを読み取り、ファイルチャネルを取り出して中身を走査する。

    private static void doBench(AbstractFileBenchContext ctx, ByteBuffer buf)
            throws IOException {
        try (FileChannel channel = (FileChannel) Files.newByteChannel(
                ctx.getTempFile(), READ)) {
            ctx.verify(runByteBuffer(channel, buf));
        }
    }

ヒープバッファ、ダイレクトバッファによるテスト

    @Benchmark
    public void testHeapBufferLE8k(BenchContextLE8k ctx) throws IOException {
        doBench(ctx, ctx.getBufHeap());
    }

    @Benchmark
    public void testDirectBufferLE8k(BenchContextLE8k ctx) throws IOException {
        doBench(ctx, ctx.getBufDirect());
    }
旧式のストリーム系APIによる読み取り

FileInputStream → BufferedInputStream → DataInputStream の連携でデータを読み取るテスト。

DataInputStreamはBigEndian専用なので、BigEndianのデータのみテストする。


DataInputStreamは一般的にいって使用頻度が高いとはいえないと思うが、byte[]からint値を取り出すための、昔からある、由緒正しい方法の1つなので、とりあえず、これで計測してみる。

    private static long readInts(InputStream is, int cnt) throws IOException {
        long total = 0;
        try (DataInputStream dis = new DataInputStream(is)) {
            for (int idx = 0; idx < cnt; idx++) {
                total += dis.readInt();
            }
        }
        return total;
    }

    @Benchmark
    public void testOldBufferedIO8k(BenchContextBE1m ctx) throws IOException {
        long total;
        try (BufferedInputStream bis = new BufferedInputStream(
                new FileInputStream(ctx.getTempFile().toFile()))) {
            total = readInts(bis, BenchContextBase.reqSize / 4);
        }
        ctx.verify(total);
    }

    @Benchmark
    public void testOldBufferedNIO8k(BenchContextBE1m ctx) throws IOException {
        long total;
        try (BufferedInputStream bis = new BufferedInputStream(
                Files.newInputStream(ctx.getTempFile()))) {
            total = readInts(bis, BenchContextBase.reqSize / 4);
        }
        ctx.verify(total);
    }

なお、Java7から、FilesクラスのnewInputStreamメソッドを使って、簡易にファイルを開くことができるようになったが、従来のFileInputStreamのコンテストラクタで開く場合と変わりないのか気になったので、そのテストも追加している。


また、FileInputStreamをBufferedInputStreamでラップしなかった場合、パフォーマンスの低下がどの程度なものなのか気になったので、こちらもテストを追加している。


計測結果

Intデータの読み取り
Benchmark                                                Mode  Cnt    Score    Error  Units
FileBufferAccessIntBenchmark.testDirectBufferBE128      thrpt   10  312.589 ±  4.157  ops/s
FileBufferAccessIntBenchmark.testDirectBufferBE16       thrpt   10  242.295 ±  3.186  ops/s
FileBufferAccessIntBenchmark.testDirectBufferBE1m       thrpt   10  317.480 ±  2.527  ops/s
FileBufferAccessIntBenchmark.testDirectBufferBE256      thrpt   10  313.295 ±  8.739  ops/s
FileBufferAccessIntBenchmark.testDirectBufferBE32       thrpt   10  294.236 ±  1.012  ops/s
FileBufferAccessIntBenchmark.testDirectBufferBE512      thrpt   10  312.779 ±  5.954  ops/s
FileBufferAccessIntBenchmark.testDirectBufferBE64       thrpt   10  307.159 ±  4.509  ops/s
FileBufferAccessIntBenchmark.testDirectBufferBE8k       thrpt   10  202.106 ±  0.781  ops/s
FileBufferAccessIntBenchmark.testDirectBufferLE128      thrpt   10  364.260 ± 10.193  ops/s
FileBufferAccessIntBenchmark.testDirectBufferLE16       thrpt   10  264.034 ±  0.895  ops/s
FileBufferAccessIntBenchmark.testDirectBufferLE1m       thrpt   10  360.495 ± 13.502  ops/s
FileBufferAccessIntBenchmark.testDirectBufferLE256      thrpt   10  362.586 ±  9.963  ops/s
FileBufferAccessIntBenchmark.testDirectBufferLE32       thrpt   10  327.563 ±  4.147  ops/s
FileBufferAccessIntBenchmark.testDirectBufferLE512      thrpt   10  359.784 ± 15.424  ops/s
FileBufferAccessIntBenchmark.testDirectBufferLE64       thrpt   10  355.960 ±  9.823  ops/s
FileBufferAccessIntBenchmark.testDirectBufferLE8k       thrpt   10  221.521 ±  0.949  ops/s
FileBufferAccessIntBenchmark.testHeapBufferBE128        thrpt   10  144.247 ±  0.341  ops/s
FileBufferAccessIntBenchmark.testHeapBufferBE16         thrpt   10  132.583 ±  0.299  ops/s
FileBufferAccessIntBenchmark.testHeapBufferBE1m         thrpt   10  162.469 ±  0.401  ops/s
FileBufferAccessIntBenchmark.testHeapBufferBE256        thrpt   10  162.569 ±  0.869  ops/s
FileBufferAccessIntBenchmark.testHeapBufferBE32         thrpt   10  135.383 ±  8.522  ops/s
FileBufferAccessIntBenchmark.testHeapBufferBE512        thrpt   10  158.935 ±  1.510  ops/s
FileBufferAccessIntBenchmark.testHeapBufferBE64         thrpt   10  136.175 ±  1.539  ops/s
FileBufferAccessIntBenchmark.testHeapBufferBE8k         thrpt   10  110.557 ±  1.032  ops/s
FileBufferAccessIntBenchmark.testHeapBufferLE128        thrpt   10  139.430 ±  0.641  ops/s
FileBufferAccessIntBenchmark.testHeapBufferLE16         thrpt   10  141.571 ±  0.264  ops/s
FileBufferAccessIntBenchmark.testHeapBufferLE1m         thrpt   10  161.376 ±  1.468  ops/s
FileBufferAccessIntBenchmark.testHeapBufferLE256        thrpt   10  164.129 ±  0.593  ops/s
FileBufferAccessIntBenchmark.testHeapBufferLE32         thrpt   10  139.513 ±  1.686  ops/s
FileBufferAccessIntBenchmark.testHeapBufferLE512        thrpt   10  164.563 ±  0.675  ops/s
FileBufferAccessIntBenchmark.testHeapBufferLE64         thrpt   10  142.430 ±  0.715  ops/s
FileBufferAccessIntBenchmark.testHeapBufferLE8k         thrpt   10  115.688 ±  0.349  ops/s
FileBufferAccessIntBenchmark.testOldBufferedIO128k      thrpt   10   21.981 ±  0.201  ops/s
FileBufferAccessIntBenchmark.testOldBufferedIO16k       thrpt   10   21.746 ±  0.176  ops/s
FileBufferAccessIntBenchmark.testOldBufferedIO32k       thrpt   10   21.734 ±  0.249  ops/s
FileBufferAccessIntBenchmark.testOldBufferedIO64k       thrpt   10   21.992 ±  0.109  ops/s
FileBufferAccessIntBenchmark.testOldBufferedIO8k        thrpt   10   21.298 ±  0.168  ops/s
FileBufferAccessIntBenchmark.testOldBufferedNIO128k     thrpt   10   22.111 ±  0.117  ops/s
FileBufferAccessIntBenchmark.testOldBufferedNIO16k      thrpt   10   21.814 ±  0.188  ops/s
FileBufferAccessIntBenchmark.testOldBufferedNIO32k      thrpt   10   22.020 ±  0.252  ops/s
FileBufferAccessIntBenchmark.testOldBufferedNIO64k      thrpt   10   22.072 ±  0.300  ops/s
FileBufferAccessIntBenchmark.testOldBufferedNIO8k       thrpt   10   21.420 ±  0.185  ops/s
FileBufferAccessIntBenchmark.testOldPlainIO             thrpt   10    0.077 ±  0.001  ops/s
FileBufferAccessIntBenchmark.testOldPlainNIO            thrpt   10    0.072 ±  0.001  ops/s
Doubleデータの読み取り
Benchmark                                                Mode  Cnt    Score    Error  Units
FileBufferAccessDoubleBenchmark.testDirectBufferBE128   thrpt   10  350.916 ±  2.222  ops/s
FileBufferAccessDoubleBenchmark.testDirectBufferBE16    thrpt   10  273.792 ±  2.641  ops/s
FileBufferAccessDoubleBenchmark.testDirectBufferBE1m    thrpt   10  350.331 ±  2.718  ops/s
FileBufferAccessDoubleBenchmark.testDirectBufferBE256   thrpt   10  351.351 ±  3.570  ops/s
FileBufferAccessDoubleBenchmark.testDirectBufferBE32    thrpt   10  322.476 ±  1.697  ops/s
FileBufferAccessDoubleBenchmark.testDirectBufferBE512   thrpt   10  347.777 ±  5.238  ops/s
FileBufferAccessDoubleBenchmark.testDirectBufferBE64    thrpt   10  347.016 ±  2.038  ops/s
FileBufferAccessDoubleBenchmark.testDirectBufferBE8k    thrpt   10  220.529 ±  0.839  ops/s
FileBufferAccessDoubleBenchmark.testDirectBufferLE128   thrpt   10  346.273 ±  7.606  ops/s
FileBufferAccessDoubleBenchmark.testDirectBufferLE16    thrpt   10  275.622 ±  1.436  ops/s
FileBufferAccessDoubleBenchmark.testDirectBufferLE1m    thrpt   10  345.511 ±  6.223  ops/s
FileBufferAccessDoubleBenchmark.testDirectBufferLE256   thrpt   10  348.193 ± 14.641  ops/s
FileBufferAccessDoubleBenchmark.testDirectBufferLE32    thrpt   10  321.897 ±  2.498  ops/s
FileBufferAccessDoubleBenchmark.testDirectBufferLE512   thrpt   10  354.947 ±  2.515  ops/s
FileBufferAccessDoubleBenchmark.testDirectBufferLE64    thrpt   10  339.376 ±  5.021  ops/s
FileBufferAccessDoubleBenchmark.testDirectBufferLE8k    thrpt   10  218.614 ±  2.389  ops/s
FileBufferAccessDoubleBenchmark.testHeapBufferBE128     thrpt   10  135.477 ±  0.477  ops/s
FileBufferAccessDoubleBenchmark.testHeapBufferBE16      thrpt   10  128.826 ±  0.493  ops/s
FileBufferAccessDoubleBenchmark.testHeapBufferBE1m      thrpt   10  133.019 ±  1.100  ops/s
FileBufferAccessDoubleBenchmark.testHeapBufferBE256     thrpt   10  135.249 ±  0.382  ops/s
FileBufferAccessDoubleBenchmark.testHeapBufferBE32      thrpt   10  138.792 ±  0.382  ops/s
FileBufferAccessDoubleBenchmark.testHeapBufferBE512     thrpt   10  133.881 ±  1.120  ops/s
FileBufferAccessDoubleBenchmark.testHeapBufferBE64      thrpt   10  135.730 ±  0.320  ops/s
FileBufferAccessDoubleBenchmark.testHeapBufferBE8k      thrpt   10  110.442 ±  0.632  ops/s
FileBufferAccessDoubleBenchmark.testHeapBufferLE128     thrpt   10  134.690 ±  0.560  ops/s
FileBufferAccessDoubleBenchmark.testHeapBufferLE16      thrpt   10  128.883 ±  0.583  ops/s
FileBufferAccessDoubleBenchmark.testHeapBufferLE1m      thrpt   10  132.606 ±  1.049  ops/s
FileBufferAccessDoubleBenchmark.testHeapBufferLE256     thrpt   10  134.812 ±  0.587  ops/s
FileBufferAccessDoubleBenchmark.testHeapBufferLE32      thrpt   10  138.150 ±  0.572  ops/s
FileBufferAccessDoubleBenchmark.testHeapBufferLE512     thrpt   10  134.951 ±  0.408  ops/s
FileBufferAccessDoubleBenchmark.testHeapBufferLE64      thrpt   10  135.497 ±  0.874  ops/s
FileBufferAccessDoubleBenchmark.testHeapBufferLE8k      thrpt   10  111.449 ±  0.286  ops/s
FileBufferAccessDoubleBenchmark.testOldBufferedIO128k   thrpt   10   46.183 ±  0.480  ops/s
FileBufferAccessDoubleBenchmark.testOldBufferedIO16     thrpt   10   42.791 ±  0.701  ops/s
FileBufferAccessDoubleBenchmark.testOldBufferedIO32     thrpt   10   45.116 ±  0.071  ops/s
FileBufferAccessDoubleBenchmark.testOldBufferedIO64     thrpt   10   45.764 ±  0.452  ops/s
FileBufferAccessDoubleBenchmark.testOldBufferedIO8k     thrpt   10   43.101 ±  0.208  ops/s
FileBufferAccessDoubleBenchmark.testOldBufferedNIO128k  thrpt   10   46.364 ±  0.373  ops/s
FileBufferAccessDoubleBenchmark.testOldBufferedNIO16    thrpt   10   44.852 ±  0.131  ops/s
FileBufferAccessDoubleBenchmark.testOldBufferedNIO32    thrpt   10   46.075 ±  0.125  ops/s
FileBufferAccessDoubleBenchmark.testOldBufferedNIO64    thrpt   10   46.895 ±  0.301  ops/s
FileBufferAccessDoubleBenchmark.testOldBufferedNIO8k    thrpt   10   43.116 ±  0.320  ops/s
FileBufferAccessDoubleBenchmark.testOldIO               thrpt   10    0.560 ±  0.006  ops/s
FileBufferAccessDoubleBenchmark.testOldNIO              thrpt   10    0.563 ±  0.008  ops/s
パフォーマンスの比較

10MbytesのファイルのInt値の連続読み取りパフォーマンスのチャート。

縦軸はテストメソッド(10MBytesファイルの読み取り)の1秒間あたりの実行回数の平均を示す。

f:id:seraphy:20151121025647p:image


やはり、ダイレクトバッファはヒープバッファよりも2倍以上性能が高い。

またバッファサイズとしては256KiB以上確保したほうが読み込み性能は良いように見える。しかし、256KiB以上確保しても、それ以上は性能は変わらないようでもある。


10MbytesのファイルのDouble値の連続読み取りパフォーマンスのチャート。

縦軸はテストメソッド(10MBytesファイルの読み取り)の1秒間あたり実行回数の平均を示す。

f:id:seraphy:20151121025648p:image


やはり、ダイレクトバッファはヒープバッファよりも2倍以上性能が高い。

また、こちらもバッファサイズとしては256KiB以上確保したほうが読み込み性能は良いように見える。しかし、256KiB以上確保しても、やはり、それ以上は性能は変わらないようでもある。

OldBufferedIOの性能が思った以上に低い

今回のベンチマークで思い知ったことは、Javaの古典的なストリーム系のBufferedInputStreamとDataInputStreamの組み合わせが、相当にパフォーマンスが低い、ということだった。

本当に予想外に性能がかなり悪い。

ヒープ上のByteBufferと比較しても、ここまでパフォーマンスが悪いとは思わなかった。


また、うっかりBufferedInputStreamで包むのを忘れてしまったFileInputStreamの性能が大変悲惨であることをチャートで見るとよく分かる。

(これは想定されていたことだが、実際にチャートで比較してみると"悲惨"という言葉に尽きる性能である。)


ただ、Java7で追加されたFiles#newInputStream()はPathで示されるファイルをオープンしてInputStreamを返すコンビニエンスなメソッドだが、コンビニエンスメソッドとして用意されたのであれば、何らかのパフォーマンスのために最低限の構成ができたものが返ってきている可能性もあるのでは?と思ってみたのだが、ぜんぜんそうゆうことはなくて、ただのnew FileInputStream()の代わりにすぎなかったようだ。

(ドキュメントにかかれてないので、まあ、そうだろう、とは思っていたけれど、全然コンビニエンスじゃないところが違和感があるというか。)


また、BufferedInputStreamはデフォルトで8KiBのバッファサイズをもっているが、これを増やしても、ほとんど性能は向上しなさそうだということも判明した。

(なので、たぶん、大多数の人が引数でバッファサイズを指定していないのだろう。)

結論

今回の実験により、以下のことが分かった。

  • ダイレクトバッファは確保と破棄に注意が必要である。
    • ダイレクトメモリサイズはヒープとは別のJavaVMオプションで設定できる。
    • 確保と破棄が連続する場合では解放が追い付かずOutOfMemoryErrorの発生の恐れがある。
      • System.gc()または内部クラスのCleanerを利用する対策がある。
    • ダイレクトバッファは逐次構築するのではなくキャッシュすると効率が良い。
  • ダイレクトバッファ > ヒープバッファ >> 古典的なストリームI/O という性能差のようである。

実のところ、ダイレクトバッファのベンチマークをとるのは、これで2〜3回目ぐらいである。

しかし、過去のテストでは、ダイレクトバッファは使いどころが難しい、うまくパフォーマンスを発揮させられない、という結論を出してしまっていた。

(今思えばベンチマークとして適切でない部分の時間も含んでいたような気もする。)

また、ヒープ上のByteBufferについても、同じくヒープ上で動作する古典的ストリームI/Oとパフォーマンスに大きな差はないだろう、という先入観のようなものもあったのかもしれない。


だが、今回のテスト結果をみると、どうも過去のテストは明らかに何かを間違えていたかのように思われる。

今回、確実に、かなり高速でデータの読み込みができることの確証を得た。


今後は、ByteBufferがうまくマッチする場合は、優先的にByteBufferによるアクセスに切り替えて行く方が良さそうだ、という教訓が得られたと思う。

今思うことは、どうして、もっと早く再評価してみようと思わなかったのか、ということだろうか。


※ 今回のベンチマーク等のソースコードは、https://gist.github.com/seraphy/c173e3d82afd1ad3f81c にあります。


以上、メモ終了。

*1:ただし、RandomAccessFileから取得されるMappedByteBufferについては、特定のファイルに結びついているので、これは不要になったら、すぐに破棄するべきである。また、ネイティブメモリを早期に解放させるためにgcをかけることが望ましいだろう。これはOSのネイティブなハンドルが閉じられていないと使用中のままになるため、ファイルの読み書き削除に支障がでるためである。

2015-11-14 jmhマイクロベンチマークの利用法とByteBufferのバイトオーダの性能

[] jmhマイクロベンチマークツールを使ったByteBufferのバイトオーダによるパフォーマンスの違いの比較  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


f:id:seraphy:20151114152414p:image


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


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


f:id:seraphy:20151114152415p:image


こうしてプロジェクトが作成されるので、まずはpom.xmlを修正して、

java8をターゲットに変えておく。

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


f:id:seraphy:20151114152416p:image


(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

f:id:seraphy:20151114152417p:image

(画面キャプチャを取り損ねたので素の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のデコードが、かなり速い


f:id:seraphy:20151114152413p:image:w564


そして、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

f:id:seraphy:20151116115140p:image:w564


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

f:id:seraphy:20151116115141p:image:w564

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

f:id:seraphy:20151116161107p:image:w564

(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

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

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

2015-04-11 JavaFX8で簡単なアプリケーションを簡単に作る方法

[][]JavaFX8で簡単なアプリケーションを簡単に作る方法 JavaFX8で簡単なアプリケーションを簡単に作る方法を含むブックマーク

動機、なぜJavaFXでアプリを作るのか?

Javaには、Swingという、十分に成熟しておりパフォーマンスも悪くないUIライブラリがある。

現状、普通に使っている分には何の不自由もない。

また、Swingは今後発展することはないとはいえ、(すぐに)廃止されるわけではない。*1

ならば、どうしてJavaFXなどという、新しいUIライブラリを覚えなければならないのか。

見るからにめんどくさそうなFXMLというマークアップ言語まで付いている。

....。

と、思っていたのだけれど、

実際に使ってみたらSwingよりも思いのほか簡単だった。


案ずるより産むが易し、というやつである。


JavaFXを使うメリット

実際にJavaFXを使ってみて、Swingに対して以下のようなメリットを感じることができた。

  • FXMLを使うことによりUIのレイアウト構造をコードから分離することができる
    • Swingではロジックとレイアウトがごちゃまぜになっていて、作るのは良いが、修正する場合に困難が伴わなかったことがない。
    • その点、JavaFXはコードではレイアウトしないので保守性が高そうである。
  • FXMLはXML構造であるため、GUIのようなオブジェクトツリー構造を記述するのに適している
    • XMLであるためツールとの親和性が高い。(Scene Builderという外部ツールでレイアウトを編集できる。)
    • やってることも、実は単純だったりする。(手で作るのも難しくないと思われる。やりたくはないけど。)
  • バインディングはめんどくさい反面、とても便利で強力だったりする。
  • CSSによる細かなレイアウトの調整やデコレーションが、プログラムコードとは分離して表現できる。
    • (ただしウェブ的なcssとは、ちょっと違う。)

上記のようなメリットを実感したため、今後はJavaFX8を優先的に使ってゆこうと思い始めている。


ここでは、これまで経験した、JavaFXで簡単にアプリを作るための簡単な方法について、軽くまとめておきたいと思う。

(また、先日のJava Day Tokyo 2015の櫻庭さんのセッションで得られた新たな知見も追記しておく。)


用意するもの

  • JDK8u40以降
  • Scene Builder 2.0
  • IDE
    • Eclipse 4.4 Luna + e(fx)clipse
    • NetBeans8
JDK8u40を使う理由

JavaFX2はJava7より利用可能になっているが、ここではJava8u40以降を使う。


Java7からJavaFX2が同梱され利用可能だったが、JavaFXのランタイムがクラスパスに通っていないため、通常の方法ではアプリケーションを配布することも実行することもめんどくさかった。

しかし、Java8からはJavaFX8は標準のクラスパスに入っており、単純に実行可能jarに丸めて配布しても動くようになっている。

JavaFX2(Java7)時代のJavaFXのビルドと配備方法は大変複雑だったが、JavaFX8では、ただのアプリと同じである。


また、JavaFX2にはメッセージボックスのような、よく使われるダイアログが(何故か)存在しない。


これも、Java8u40から、JavaFX Dialogsの成果物Alertクラスが標準に取り込まれたので、メッセージボックスの表示も標準でできるようになっている。

http://code.makery.ch/blog/javafx-dialogs-official/

※ Java8u40未満では上記のJavaFXDialogsのライブラリを使うと良いだろう。


Scene Builder 2.0

Scene Builderは、JavaFXGUIをレイアウトするためのツールである。

このツールを使うことにより、コンポーネントの配置状態を表すFXMLというXMLファイルをグラフィカルに編集、保存することができる。


これはJDKには付属していないので、別途ダウンロードする必要がある。

http://www.oracle.com/technetwork/java/javase/downloads/javafxscenebuilder-info-2157684.html

ただし、現行のSceneBuilder2はFXMLのすべての機能をサポートしているわけではなく、

手作業で手の込んだFXMLを作るとScene Builderでは開けなくなったりするので注意が必要である。


簡単なアプリケーションを作るうえでは、Scene Builderだけで十分ではないか、と思う。


なお、最新のScene BuilderのバイナリはOracleからは配布されておらず、OpenJDK側でソースとして提供されている。

これを使うには、自分でビルドするか、だれかがビルドしてくれたバイナリをもらってくる必要がある。*2

Scene Builder自身もJavaFXで書かれているので、jdk8環境があればビルドできるようである。*3


IDE

JavaFX8は、ふつうのJavaコード + FXMLというXMLファイルで作れるので、原理的には何のIDEでも良い。


Eclipseの場合は、Java8をサポートするLuna以降が必要であり、且つ、FXMLのサポートがつく"e(fx)clipse"プラグインを入れる。

Eclipsejavafxで始まるパッケージを非公開クラスの利用だとしてエラー表示するが、このプラグインを入れると解消される。

設定から、Scene Builderの実行ファイルの位置などを指定する必要がある。


NetBeans8の場合でも、FXMLの編集にはScene Builderを使うので、ツールメニューからオプションを開き、JavaのタブからScene Builderのホームディレクトリを指定する必要がある。

これでIDEとScene Builderとの連携ができるようになる。

(なお、NetBeans8でJavaFXプロジェクトをMavenで作ると、Java7時代のめんどくさいビルドスクリプトが出来上がるの注意。JavaFX8では、普通のアプリのビルド方法と同じで良い。)


JavaFXのお約束

エントリポイント

従来、SwingのJFrameを使った簡単なアプリケーションは以下のような形で作っていたと思う。

class MyFrame extends JFrame {

    public MyFrame() {
        // 初期化処理
    }

    public static void main(String... args) throws Exception {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                MyFrame inst = new MyFrame();
                inst.setLocationByPlatform(true);
                inst.setVisible(true);
            }
        });
    }
}

mainメソッドでイベントディスパッチスレッド(EDT)に入ってJFrameを作成する、という流れである。

(手抜きなコード例だと、EDTに入らずに、いきなりnew JFrameしていたりする例も散見されるが、これは禁止事項である。)


これに対して、JavaFXでは、アプリケーションのエントリポイントは、かならず以下のような形式になる。

package jp.seraphyware.simpleapp0;

import javafx.application.Application;
import javafx.stage.Stage;

public class SimpleApp extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {
        // ここからスタートする
    }

    public static void main(String... args) {
        launch(args);
    }
}

まず、アプリケーションは、必ずApplicationを継承したクラスになる。

JavaFXに対応したランチャは、Javaアプレットのようにmainは経由せず、startから開始する。*4


mainから開始する古典的な実行形式の場合には上記のように単にlaunchを呼ぶ形にしておく。

これにより、JavaFXアプリケーションスレッドに入ったあとでstartメソッドが呼び出されるようになる。

(この場合でSwingと異なる点は、launchメソッドを呼んだ後、JavaFXアプリケーションスレッドが終了するまで制御が返らない点であろう。)


startメソッドは呼び出された時点でJavaFXアプリケーションスレッド上にいる。


引数にはStageが1つ受け取られているが、これがSwingでいうところのJFrameに相当する、メインウィンドウとなる。

このstartメソッド内でJavaFXコンポーネントを作成して、Stageに上に乗せることで、ウィンドウを作成する。

(アプリケーションは、必ずしも、このprimaryStageを使う必要はなく、任意にStageを作成して使うこともできるが、アプレット等でブラウザに埋め込まれている場合は、このprimaryStageが、その埋め込み領域を表している。)


コンポーネントを作成する方法としては、

のいずれの方法でもよい。

必ずFXMLを使わなければならない、というものではない。


JavaFXアプリケーションスレッドが終了する場合にはstopメソッドが呼び出されるので、ここで後始末ができる。


UIスレッド/JavaFXアプリケーションスレッド

JavaFX8では、JavaFXアプリケーションスレッド以外からUIを操作したい場合には、Swingの場合と同様にJavaFXアプリケーションスレッド上で実行するRunnableを引き渡す必要がある。

Platform#runLater(Runnable r)


JavaFXアプリケーションスレッドは、Swing/AWTのEDT(UIスレッド)とは別のスレッドである。

JavaFX8では、SwingNodeによって、JavaFXの中でSwingを利用することもできるが、

SwingとJavaFXのスレッドは別物なので、SwingNode上のSwingコンポーネントを作成、操作する場合には、必ず、SwingのUIスレッドで行う必要がある。


この場合、Swing用にはSwingUtilities#invokeLaterを引き続き使用する必要がある。



全部がObservableである。

JavaFXでは従来のJava Beansと異なり、"なんとかProperty"という形式のプロパティを持つ。

たとえば、textというプロパティを持つJavaFX Beansであれば、だいたい以下のようになる。

package jp.seraphyware.simpleapp0;

import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

public class JavaFXSimpleModel {

    private SimpleStringProperty textProperty;

    public StringProperty textProperty() {
        if (textProperty == null) {
            textProperty  = new SimpleStringProperty(this, "text");
        }
        return textProperty;
    }

    public String getText() {
        return textProperty().get();
    }

    public void setText(String text) {
        textProperty().set(text);
    }
}

javafx.beans.***Propertyクラスは、値の変更を検知可能な(Observable)値のホルダーであり、

JavaFXでは、あらゆるプロパティがObservableである。


また、これはバインド可能(一方向、双方向)であり、ある値が変更されると連結されている別のコンポーネントのプロパティも自動的に変更される、という動作ができる。

バインドは、かなり強力で、複数のプロパティの演算結果をバインドする、というようなこともできるようになっている。


JavaFXでは、とにかく全部がObservableである。


JavaFXによる簡単なGUIアプリの作成

まず、以下のような簡単なGUIを作りたいと思う。

f:id:seraphy:20150411141326p:image


まずはFXMLによるGUIのレイアウトを行う

画面のレイアウトのデザインにはScene Builderを使う。


EclipseNetBeans上で適当に空のFXMLファイルを作成し、そのファイルを右クリックして「Scene Builderで開く」みたいなメニューがあれば、それでScene Builderが開かれる。*5

f:id:seraphy:20150411141327p:image

Swingの場合はJPanelの中にレイアウトマネージャを設定してレイアウトを組んでいたが、JavaFXの場合では、はじめからGridPane, BorderPane, FlowPane, VBox, HBoxのようなレイアウトペインになっている。

この上に必要なコンポーネントを乗せて行くことで画面をデザインしてゆくことになる。


実際に画面のレイアウトするうえでの注意は「アンカーペインには頼りすぎない」ことが使ってみたところの教訓といえようか。


アンカーペイン(AnchorPane)は、Visual Studioのフォームエディタのような感覚でコンポーネントを並べてゆけるうえに、アンカーを指定することで画面サイズにあわせて拡縮するようなレイアウトも容易に作れる。

しかし、コンポーネント自身の適切なサイズは使用するフォントや文字数により変わるため、ローカライズした場合やWindows/Linux/Mac間でフォントが変わった場合などでサイズが変わるため、なかなか調整が難しい。


なので、今のところの感触では、Swingを手動でレイアウトを組んでいたころのように、GridPane, BorderPane, FlowPane, VBox, HBoxなどのレイアウトを使ってコンポーネントのサイズを自動調整させつつ、それらを組み合わせて配置してゆくのが良いだろう、と思う。


fx:idの割り当て

プログラム側からコンポーネントを制御できるようにするため「fx:id」属性で名前をつけておく必要がある。


これによりFXMLとコントローラクラス上のフィールドとが結び付けられるようになる。

  • txtDir
  • txtNamePattern
  • btnBrowse
  • chkSubdir
  • btnOK

アクション名の割り当て

ボタンのアクションに対しては、以下のようにメソッド名を割り当てておく。


これによりFXMLとコントローラクラス上のメソッドと連結されることになる。

  • #onBrowse
  • #onOK
  • #onCancel

※ ここにスクリプトを直接書くことも可能。


SceneBuilderの画面を示す。

f:id:seraphy:20150411141328p:image


出来上がったFXMLの例

できあがったFXMLは以下のようなものである。

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>

<VBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
   <children>
      <GridPane hgap="3.0" vgap="3.0">
        <columnConstraints>
          <ColumnConstraints hgrow="SOMETIMES" percentWidth="0.0" />
          <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
            <ColumnConstraints hgrow="SOMETIMES" percentWidth="0.0" />
        </columnConstraints>
        <rowConstraints>
          <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
          <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
          <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
        </rowConstraints>
         <children>
            <Label text="ディレクトリ" GridPane.halignment="RIGHT">
               <GridPane.margin>
                  <Insets right="5.0" />
               </GridPane.margin>
            </Label>
            <Label text="ファイル名パターン" GridPane.halignment="RIGHT" GridPane.rowIndex="1">
               <GridPane.margin>
                  <Insets right="5.0" />
               </GridPane.margin>
            </Label>
            <TextField fx:id="txtDir" GridPane.columnIndex="1" />
            <Button fx:id="btnBrowse" mnemonicParsing="false" onAction="#onBrowse" text="参照" GridPane.columnIndex="2" />
            <TextField fx:id="txtNamePattern" text="*.*" GridPane.columnIndex="1" GridPane.rowIndex="1" />
            <CheckBox fx:id="chkSubdir" mnemonicParsing="false" text="サブディレクトリを含む" GridPane.columnIndex="1" GridPane.rowIndex="2" />
         </children>
      </GridPane>
      <FlowPane alignment="TOP_RIGHT" hgap="5.0" vgap="5.0">
         <children>
            <Button fx:id="btnOK" defaultButton="true" mnemonicParsing="false" onAction="#onOK" text="実行" />
            <Button cancelButton="true" mnemonicParsing="false" onAction="#onCancel" text="キャンセル" />
         </children>
         <padding>
            <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
         </padding>
      </FlowPane>
   </children>
   <padding>
      <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
   </padding>
</VBox>

FXMLはとても汎用的にできていて、fxで始まる名前空間のもの以外は、特殊なものはない。

これはオブジェクトツリーを作成するための表記そのものといってよい。

http://docs.oracle.com/javafx/2/api/javafx/fxml/doc-files/introduction_to_fxml.html


FXMLのルールは、ざっくりといえば、

  • 大文字のタグはクラス名であり、そのクラスをnewすることを意味する
  • 小文字のタグまたは属性名がプロパティを意味する
    • 属性名を使う場合は文字列または数値のリテラルのみ指定可能
    • 小文字のタグを使う場合には、リスト表現や、fxで始まる名前空間を利用してJavaの定数を参照することも可能。

これらをつなげてツリーを作ることでUI要素の親子関係を表現しているのである。


また、要素の出現順で、タブキーによるフォーカス移動順(フォーカストラバーサル)が決まるなどの性質もある。


上記例では、このFXMLをロードすると、ルート要素として各種UI要素を配置済みのVBoxが返されることになる。


FXMLに対応するコントローラクラスの作成

前述のように、コンポーネントに名前を割り当てたり、ボタンのアクションのメソッド名を割り当てた場合、それらを受ける「コントローラ」クラスの定義を行う必要がある。

前述の例であれば、以下のようなコントローラが必要となる。

public class SimpleAppController implements Initializable {

    @FXML
    private TextField txtDir;

    @FXML
    private TextField txtNamePattern;

    @FXML
    private CheckBox chkSubdir;

    @FXML
    private Button btnOK;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        // OKボタンの活性制御
        btnOK.disableProperty().bind(
                Bindings.or(
                        txtDir.textProperty().isEmpty(),
                        txtNamePattern.textProperty().isEmpty()
                        ));
    }

    @FXML
    protected void onBrowse(ActionEvent evt) {
        // ここにBrowseボタンのアクションを定義する
    }

    @FXML
    protected void onOK(ActionEvent evt) {
        // ここにOKボタンのアクションを定義する
    }

    @FXML
    protected void onCancel(ActionEvent evt) {
        // 現在ボタンを表示しているシーンを表示しているステージに対して
        // クローズを要求する.
        ((Stage) ((Button) evt.getSource()).getScene().getWindow()).close();
    }
}

コントローラにするクラスには、フィールドまたはメソッドに対して@FXMLアノテーションをつけたものが、FXMLLoaderによってJavaFXコンポーネントと結び付けられる。

また、Initializableインターフェイスを実装する場合、

各種フィールドやメソッドがコンポーネントと関連づけられたあとに、initializeメソッドが呼び出されるので、ここで追加の初期化処理を行うことができる。

ここでは、2つのテキストフィールドに何らかの文字が入力されていないかぎりOKボタンを有効としないような活性制御のバインドを設定している。

(txtDirが空か、txtNamePatternが空の場合に、btnOKのdisableプロパティをTrueに設定するバインドを設定している。)


なお、onCancelメソッドでは、ウィンドウを閉じるために、やや回りくどいことをしているが、

コントローラはどこからも継承していない独立しているクラスなので、あらかじめフィールドにステージの情報を入れておくか、何かしらのUI要素を遡って、現在表示しているウィンドウを取得するなどの方法を取る必要がある。


コントローラクラスの指定方法

コントローラクラスの指定方法には、いくつかあり

  • FXMLLoaderで直接、コントローラのインスタンスを指定する (fx:controller属性は書かない)
  • FXMLコード中のルート要素のfx:controller属性にクラス名を指定する
    • FXMLLoaderでコントローラのファクトリを指定することもできる.
      • SpringなどDIコンテナ管理のコントローラを作りたい場合などは、ここでインジェクションしたあとのインスタンスを渡すなどのファクトリクラスの定義ができる。

今回は、FXMLにはコントローラクラスは記述せず、FXMLLoaderで直接コントローラのインスタンスを指定する方法を用いる。


FXMLのロード

FXMLは以下のようにしてロードすることができる。

/**
 * 簡単なFXMLアプリケーション作成例.
 *
 * @author seraphy
 */
public class SimpleApp extends Application implements Initializable {
....(前略)....

    /**
     * アプリケーション開始
     */
    @Override
    public void start(Stage primaryStage) throws Exception {
        // FXMLをリソースから取得する.
        // (タブオーダーもFXMLの定義順になる.)
        // (FXML中に「@参照」による相対位置指定がある場合は、このURL相対位置となる.)
        URL fxml = getClass().getResource(getClass().getSimpleName() + ".fxml");

        // FXMLをロードする.
        // (ローカライズしない場合はリソースバンドルを指定しなくて良い)
        FXMLLoader ldr = new FXMLLoader(fxml, null);

        // このインスタンス自身をコントローラとする.
        // @FXMLアノテーションによりFXMLと結び付けられる.
        ldr.setController(this);

        // FXMLをロードする.
        Parent root = ldr.load();

        // ステージのタイトル
        primaryStage.setTitle("SimpleApp Sample");

        // ステージの表示
        primaryStage.setScene(new Scene(root));
        primaryStage.show();

        // ディレクトリ選択テキストにフォーカスを当てる
        txtDir.requestFocus();
    }

....(後略)....
}

FXMLLoaderクラスによってFXMLをロードできる。

FXMLファイルの指定方法にはURLによる方法とストリームによる方法などがあるが、基本はURLによる指定を使うほうが良い。


これはFXML表記で"@picture1.png"のような形式でFXMLファイルからの相対位置でリソースのありかを指定する表記方法があるが、

ストリームでFXMLを読み込ませる場合には、その位置の指定が取れなくなるためである。


FXMLをロードしフィールドやメソッドは、setControllerメソッドに対してthisを渡すことで明示している。

これにより、このクラス自身がコントローラを兼ねることになる。


あとは、これをloadすると、FXMLの最初の要素である"VBox"のインスタンスが得られるので、

これをSceneオブジェクトに入れて、それをStageにセットすることで、画面上に表示できるようになる。


※なお、FXMLLoaderはオブジェクトツリーの汎用ファクトリであり、任意の独自のクラスや、あるいはStageそのものをルートとしてオブジェクトツリーを作成することも可能である。

(ただし、その場合はSceneBuilderでは編集できなくなるので、実用性は少ないかもしれない。)


完全なソースコード
package jp.seraphyware.simpleapp1;

import java.io.File;
import java.net.URL;
import java.util.ResourceBundle;

import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.TextField;
import javafx.stage.DirectoryChooser;
import javafx.stage.Stage;
import jp.seraphyware.utils.ErrorDialogUtils;

/**
 * 簡単なFXMLアプリケーション作成例.
 *
 * @author seraphy
 */
public class SimpleApp extends Application implements Initializable {

    @FXML
    private TextField txtDir;

    @FXML
    private TextField txtNamePattern;

    @FXML
    private CheckBox chkSubdir;

    @FXML
    private Button btnOK;

    /**
     * ディレクトリ選択ダイアログ
     */
    private DirectoryChooser dirChooser = new DirectoryChooser();

    /**
     * アプリケーション開始
     */
    @Override
    public void start(Stage primaryStage) throws Exception {
        // FXMLをリソースから取得する.
        // (タブオーダーもFXMLの定義順になる.)
        // (FXML中に「@参照」による相対位置指定がある場合は、このURL相対位置となる.)
        URL fxml = getClass().getResource(getClass().getSimpleName() + ".fxml");

        // FXMLをロードする.
        // (ローカライズしない場合はリソースバンドルを指定しなくて良い)
        FXMLLoader ldr = new FXMLLoader(fxml, null);

        // このインスタンス自身をコントローラとする.
        // @FXMLアノテーションによりFXMLと結び付けられる.
        ldr.setController(this);

        // FXMLをロードする.
        Parent root = ldr.load();

        // ステージのタイトル
        primaryStage.setTitle("SimpleApp Sample");

        // ステージの表示
        primaryStage.setScene(new Scene(root));
        primaryStage.show();

        // ディレクトリ選択テキストにフォーカスを当てる
        txtDir.requestFocus();
    }

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        // ダイアログのタイトルの設定
        dirChooser.setTitle("フォルダの選択");

        // OKボタンの活性制御
        btnOK.disableProperty().bind(
                Bindings.or(
                        txtDir.textProperty().isEmpty(),
                        txtNamePattern.textProperty().isEmpty()
                        ));
    }

    @FXML
    protected void onBrowse(ActionEvent evt) {
        try {
            String srcDir = txtDir.textProperty().get();
            if (srcDir != null && !srcDir.isEmpty()) {
                File initDir = new File(srcDir);
                if (initDir.isDirectory()) {
                    dirChooser.setInitialDirectory(initDir);
                }
            }
            File selectedDir = dirChooser.showDialog(((Button) evt.getSource())
                    .getScene().getWindow());
            if (selectedDir != null) {
                txtDir.setText(selectedDir.getAbsolutePath());
                txtDir.requestFocus();
            }

        } catch (Exception ex) {
            ErrorDialogUtils.showException(ex);
        }
    }

    @FXML
    protected void onOK(ActionEvent evt) {
        Alert alert = new Alert(AlertType.INFORMATION);
        alert.setHeaderText("実行!");

        StringBuilder buf = new StringBuilder();
        buf.append("dir=").append(txtDir.getText()).append("\r\n")
                .append("namePattern=").append(txtNamePattern.getText())
                .append("\r\n").append("subdir=")
                .append(chkSubdir.isSelected() ? "Yes" : "No");
        alert.setContentText(buf.toString());

        alert.showAndWait();
    }

    @FXML
    protected void onCancel(ActionEvent evt) {
        // 現在ボタンを表示しているシーンを表示しているステージに対して
        // クローズを要求する.
        ((Stage) ((Button) evt.getSource()).getScene().getWindow()).close();
    }

    /**
     * エントリポイント
     * @param args
     * @throws Exception
     */
    public static void main(String... args) throws Exception {
        launch(args);
    }
}

このように、単純なアプリであればコントローラ兼メインクラスとなるクラス1つと、レイアウトを定めているFXMLリソースが1つあれば簡単に作ることができる。

FXMLファイルの中身もわかってしまえば簡単である。


JavaFXでやってみると、同等な画面をSwingで手作業で作るよりも簡単であることが分かった。


リソースバンドルを使った簡単なアプリの作成例

JavaFXアプリケーションをローカライズする場合にはリソースバンドルを指定することで簡単に対応できるようになっている。

FXML上に文字列リテラルを書く代わりに、"%resource.key"のような形式とすることで、リソースバンドルより自動的に文字列を引き当てることができるのである。

文字列リテラルからリソースキーに変更したFXML
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>

<VBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
   <children>
      <GridPane hgap="3.0" vgap="3.0">
        <columnConstraints>
          <ColumnConstraints hgrow="SOMETIMES" percentWidth="0.0" />
          <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
            <ColumnConstraints hgrow="SOMETIMES" percentWidth="0.0" />
        </columnConstraints>
        <rowConstraints>
          <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
          <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
          <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
        </rowConstraints>
         <children>
            <Label text="%directory" GridPane.halignment="RIGHT">
               <GridPane.margin>
                  <Insets right="5.0" />
               </GridPane.margin>
            </Label>
            <Label text="%namePattern" GridPane.halignment="RIGHT" GridPane.rowIndex="1">
               <GridPane.margin>
                  <Insets right="5.0" />
               </GridPane.margin>
            </Label>
            <TextField fx:id="txtDir" GridPane.columnIndex="1" />
            <Button fx:id="btnBrowse" mnemonicParsing="false" onAction="#onBrowse" text="%browse" GridPane.columnIndex="2" />
            <TextField fx:id="txtNamePattern" text="*.*" GridPane.columnIndex="1" GridPane.rowIndex="1" />
            <CheckBox fx:id="chkSubdir" mnemonicParsing="false" text="%includeSubDirs" GridPane.columnIndex="1" GridPane.rowIndex="2" />
         </children>
      </GridPane>
      <FlowPane alignment="TOP_RIGHT" hgap="5.0" vgap="5.0">
         <children>
            <Button fx:id="btnOK" defaultButton="true" mnemonicParsing="false" onAction="#onOK" text="%ok" />
            <Button cancelButton="true" mnemonicParsing="false" onAction="#onCancel" text="%cancel" />
         </children>
         <padding>
            <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
         </padding>
      </FlowPane>
   </children>
   <padding>
      <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
   </padding>
</VBox>

これは前述のFXMLから文字列リテラルを全て%resource.keyの形式に置き換えただけである。


リソースバンドルの定義

リソースバンドルには、ここではXML形式のリソースバンドルを使用するものとする。

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>SimpleApp Resource</comment>
<entry key="title">SimpleApp サンプル#2</entry>
<entry key="dirChooser.title">フォルダの選択</entry>

<entry key="directory">ディレクトリ</entry>
<entry key="browse">参照</entry>
<entry key="namePattern">ファイル名パターン</entry>
<entry key="includeSubDirs">サブディレクトリを含む</entry>

<entry key="ok">実行</entry>
<entry key="cancel">キャンセル</entry>
</properties>

※ 本サンプルで使っているXMLResourceBundleControlは、XMLプロパティファイル形式をリソースバンドルとして読み込めるようにリソースバンドルコントローラを拡張した実装であるが、本質的に、ただのリソースバンドルでも同じである。

(実装例: https://gist.github.com/seraphy/b421ac03d64d541667b2)

FXMLLoader側の修正

FXMLLoader側は、リソースバンドルを指定すれば良いだけである。

    @Override
    public void start(Stage primaryStage) throws Exception {
        // FXMLをリソースから取得する.
        // (タブオーダーもFXMLの定義順になる.)
        // (FXML中に「@参照」による相対位置指定がある場合は、このURL相対位置となる.)
        URL fxml = getClass().getResource(getClass().getSimpleName() + ".fxml");

        // XMLリソースバンドルを読み込む
        ResourceBundle rb = ResourceBundle.getBundle(getClass().getName(),
                new XMLResourceBundleControl());

        // FXMLをロードする.
        // (ローカライズするのでリソースバンドルも指定する)
        FXMLLoader ldr = new FXMLLoader(fxml, rb);

        // このインスタンス自身をコントローラとする.
        // @FXMLアノテーションによりFXMLと結び付けられる.
        ldr.setController(this);

        // FXMLをロードする.
        Parent root = ldr.load();

        // ステージのタイトル
        primaryStage.setTitle(rb.getString("title"));

        // ステージの表示
        primaryStage.setScene(new Scene(root));
        primaryStage.show();

        // ディレクトリ選択テキストにフォーカスを当てる
        txtDir.requestFocus();
    }

見ての通り、コード側は、ほとんど修正はない。


ただリソースバンドルを読み込むだけで、%resource.keyを取得する部分はFXMLLoaderが全部やってくれている。


JavaFXでやればFXMLのリテラル修正とリソースバンドルの読み込み、これだけでローカライズに対応できるので、

SwingでリソースバンドルのgetStringを延々と手作業で記述するよりもはるかに簡単にできることが分かる。


カスタムコンポーネントの作成

JavaFXではFXMLを使ったカスタムコンポーネントの定義も考慮されている。

ここでは、テキストフィールドと参照ボタンの2つのコンポーネントを組み合わせた「DirTextField」というカスタムコンポーネントを定義してみる。

f:id:seraphy:20150411141329p:image

2つのコンポーネントをもつカスタムコンポーネント用のFXML
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>

<fx:root type="javafx.scene.layout.BorderPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
   <center>
      <TextField fx:id="txtDir" BorderPane.alignment="CENTER">
         <opaqueInsets>
            <Insets />
         </opaqueInsets>
         <BorderPane.margin>
            <Insets right="5.0" />
         </BorderPane.margin></TextField>
   </center>
   <right>
      <Button mnemonicParsing="false" onAction="#onBrowse" text="%browse" BorderPane.alignment="CENTER" />
   </right>
</fx:root>

こちらも、Scene Builder2で編集可能である。


サイズ可変でも対応できるように、BorderPaneでセンターにテキスト、Rightにボタンを配置している。


今までと違うのは、ルート要素が「fx:root」というタグになっており、type属性にBorderPaneのクラスを指定している。

これにより、Scene Builder上では、type属性に示されたBorderPaneであるかのようにふるまう。


この意味するところは、FXMLLoaderによってロードされるときルート自身はnewされずに、親として指定された作成済みインスタンスに対して属性や子アイテムを追加してゆく、ということである。


カスタムコンポーネント用FXMLのロード

このFXMLはルート、すなわち自分自身がBorderPaneを継承するカスタムクラスであることを示しており、以下のようにBorderPaneを継承するカスタムクラスのコンストラクタでロードする形となる。

package jp.seraphyware.simpleapp3;

import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URL;
import java.util.ResourceBundle;

import javafx.beans.property.StringProperty;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.stage.DirectoryChooser;
import jp.seraphyware.utils.ErrorDialogUtils;
import jp.seraphyware.utils.XMLResourceBundleControl;

public class DirTextField extends BorderPane {

    @FXML
    private TextField txtDir;

    /**
     * ディレクトリ選択ダイアログ
     */
    private DirectoryChooser dirChooser = new DirectoryChooser();

    /**
     * コンストラクタ
     */
    public DirTextField() {
        URL url = getClass().getResource(getClass().getSimpleName() + ".fxml");
        ResourceBundle rb = ResourceBundle.getBundle(getClass().getName(),
                new XMLResourceBundleControl());
        FXMLLoader ldr = new FXMLLoader(url, rb);

        // このインスタンス自身がルートオブジェクト
        ldr.setRoot(this);

        // このインスタンス自身がコントローラ
        ldr.setController(this);

        try {
            // ルートを指定済みなので、このインスタンスにFXMLがロードされる.
            ldr.load();

        } catch (IOException ex) {
            throw new UncheckedIOException(ex);
        }

        // ダイアログのタイトル設定
        dirChooser.setTitle(rb.getString("dirChooser.title"));

        // フォーカスリスナ
        focusedProperty().addListener((o, old, newval) -> {
            if (newval) {
                txtDir.requestFocus();
            }
        });
    }

    @FXML
    protected void onBrowse(ActionEvent evt) {
        try {
            String srcDir = txtDir.textProperty().get();
            if (srcDir != null && !srcDir.isEmpty()) {
                File initDir = new File(srcDir);
                if (initDir.isDirectory()) {
                    dirChooser.setInitialDirectory(initDir);
                }
            }
            File selectedDir = dirChooser.showDialog(((Button) evt.getSource())
                    .getScene().getWindow());
            if (selectedDir != null) {
                txtDir.setText(selectedDir.getAbsolutePath());
                txtDir.requestFocus();
            }

        } catch (Exception ex) {
            ErrorDialogUtils.showException(ex);
        }
    }

    public StringProperty textProperty() {
        return txtDir.textProperty();
    }

    public String getText() {
        return textProperty().get();
    }

    public void setText(String text) {
        textProperty().set(text);
    }
}

自分自身がコントローラであることは先の例と同様である。


今回は、それに加えてルートが自分自身であることを示すためにFXMLLoader#setRoot()で自分自身をルートであるように指定する。


また、このクラス専用のリソースバンドルを読み込んでいる。


rootがthisとなっていることにより、loadメソッドによって自分自身に設定することになるため、loadメソッドの戻り値は無視して構わない。

textPropertyなどのプロパティは、このカスタムコンポーネントの持つ値を外部からアクセス可能にするためのものである。


カスタムコンポーネントの利用

カスタムコンポーネントの利用は、FXML上で、カスタムクラス名を指定すれば良いだけである。

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>
<?import jp.seraphyware.simpleapp3.*?>

<VBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
   <children>
      <GridPane hgap="3.0" vgap="3.0">
        <columnConstraints>
          <ColumnConstraints hgrow="SOMETIMES" percentWidth="0.0" />
          <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
            <ColumnConstraints hgrow="SOMETIMES" percentWidth="0.0" />
        </columnConstraints>
        <rowConstraints>
          <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
          <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
          <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
        </rowConstraints>
         <children>
            <Label text="%directory" GridPane.halignment="RIGHT">
               <GridPane.margin>
                  <Insets right="5.0" />
               </GridPane.margin>
            </Label>
            <Label text="%namePattern" GridPane.halignment="RIGHT" GridPane.rowIndex="1">
               <GridPane.margin>
                  <Insets right="5.0" />
               </GridPane.margin>
            </Label>
            <DirTextField fx:id="dirTextField" text="C:\" GridPane.columnIndex="1" />
            <TextField fx:id="txtNamePattern" text="*.*" GridPane.columnIndex="1" GridPane.rowIndex="1" />
            <CheckBox fx:id="chkSubdir" mnemonicParsing="false" text="%includeSubDirs" GridPane.columnIndex="1" GridPane.rowIndex="2" />
         </children>
      </GridPane>
      <FlowPane alignment="TOP_RIGHT" hgap="5.0" vgap="5.0">
         <children>
            <Button fx:id="btnOK" defaultButton="true" mnemonicParsing="false" onAction="#onOK" text="%ok" />
            <Button cancelButton="true" mnemonicParsing="false" onAction="#onCancel" text="%cancel" />
         </children>
         <padding>
            <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
         </padding>
      </FlowPane>
   </children>
   <padding>
      <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
   </padding>
</VBox>

この中にある「DirTextField」というタグがカスタムコンポーネントのクラス名になる。


★ちなみに、Scene Builderでは、カスタムコンポーネントは、コンポーネントペインの歯車アイコンから「Import JAR/FXML File...」を選択し、そのFXMLを内包するjarクラスをインポートすることにより、左ペインに「カスタム」の一覧にカスタムコンポーネントが使えるようになり、グラフィカルエディタ上でもコンポーネントを認識するようになる。

しかし、とくにカスタムコンポーネントをインポートしない場合でも、SceneBuilderでは得体の知らないクラスとして画面上では穴が開いた状態になり、グラフィカルな画面からの操作は不可能ではあるが、とくにエラーになるわけではない。

ここでは手抜きのため、SceneBuilderにはインポートしないものとする。

※ なお、SceneBuilder2でインポートしたカスタムコンポーネントを消すには、現状、消すコマンドがないので、%APPDATA%\Scene Builder\Library上にあるFXMLファイルかJARファイルを手動で削除する必要がある。


「fx:id」で名前を「dirTextField」と指定しているので、コントローラ側では以下のように、カスタムクラスのフィールド名で受けることができる。

    @FXML
    private DirTextField dirTextField;

実行例

f:id:seraphy:20150411141330p:image

カスタムコンポーネントのFXMLによる定義方法も利用方法も簡単である。


また、このカスタムコントロールの手法は、FXMLにカスタムコントロールを直接埋め込む場合だけでなく、VBoxのようなペインに対して、プログラム的に2個以上連続して作成させるような場合にも、とても有効な手法ではないかと考えられる。


ネストしたコントローラの利用

カスタムコンポーネントに似た別の方法として、ネストしたFXMLの利用がある。

ネストしたFXMLとは、FXMLの中で別のFXMLをincludeするものである。


includeといってもC言語のプリプロセッサ的な外部ファイルを流し込むだけの単純なincludeではなく、外部の独立したコントローラを生成して外部のFXMLを親のFXMLに埋め込むものである。


親子関係をもった状態で作成されるため、実質的に、カスタムコントロールとほとんど同じような感じとなる。


FXMLの定義

FXMLの定義は前述のカスタムコントロールの例をモデルとするが、異なるのはルート要素が「fx:root」ではなく、普通のFXMLと同様に明示的に「BorderPane」のようなクラス指定になっている、ということである。


これの意味するところは、FXMLLoaderによって実際にBorderPaneがnewされる、ということである。


<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>

<BorderPane fx:id="root" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"
	fx:controller="jp.seraphyware.simpleapp4.DirTextFieldController">
   <center>
      <TextField fx:id="txtDir" BorderPane.alignment="CENTER">
         <opaqueInsets>
            <Insets />
         </opaqueInsets>
         <BorderPane.margin>
            <Insets right="5.0" />
         </BorderPane.margin></TextField>
   </center>
   <right>
      <Button mnemonicParsing="false" onAction="#onBrowse" text="%browse" BorderPane.alignment="CENTER" />
   </right>
</BorderPane>

もう一つ、重要な点は、ルート要素に「fx:controller」による、コントローラクラスの指定がある点である。


これにより、このFXMLを構築するときに自動的に"jp.seraphyware.simpleapp4.DirTextFieldController"クラスがデフォルトコンストラクタでインスタンス化され、コントローラとして使用される。

(※ FXMLLoaderでコントローラのファクトリを指定している場合は、インスタンス取得のためにファクトリによってコントローラのインスタンスを取得する。)


コントローラの定義

FXMLの受けるコントローラは以下のように定義する。

package jp.seraphyware.simpleapp4;

import java.io.File;
import java.net.URL;
import java.util.ResourceBundle;

import javafx.beans.property.StringProperty;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.stage.DirectoryChooser;
import jp.seraphyware.utils.ErrorDialogUtils;

public class DirTextFieldController implements Initializable {

    @FXML
    private BorderPane root;

    @FXML
    private TextField txtDir;

    /**
     * ディレクトリ選択ダイアログ
     */
    private DirectoryChooser dirChooser = new DirectoryChooser();

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        // ダイアログのタイトル設定
        dirChooser.setTitle(resources.getString("dirChooser.title"));

        // フォーカスリスナ
        root.focusedProperty().addListener((o, old, newval) -> {
            if (newval) {
                txtDir.requestFocus();
            }
        });
    }

    @FXML
    protected void onBrowse(ActionEvent evt) {
        try {
            String srcDir = txtDir.textProperty().get();
            if (srcDir != null && !srcDir.isEmpty()) {
                File initDir = new File(srcDir);
                if (initDir.isDirectory()) {
                    dirChooser.setInitialDirectory(initDir);
                }
            }
            File selectedDir = dirChooser.showDialog(((Button) evt.getSource())
                    .getScene().getWindow());
            if (selectedDir != null) {
                txtDir.setText(selectedDir.getAbsolutePath());
                txtDir.requestFocus();
            }

        } catch (Exception ex) {
            ErrorDialogUtils.showException(ex);
        }
    }

    public StringProperty textProperty() {
        return txtDir.textProperty();
    }

    public String getText() {
        return textProperty().get();
    }

    public void setText(String text) {
        textProperty().set(text);
    }
}

実質的に、カスタムコントローラの場合と、ほとんど同じである。


なお、"fx:id"属性を使って、BorderPane自身を"root"という名前にしてコントローラのフィールドに割り当てているので、initializeによる初期化時にルートに対する操作も可能としている。

(この方法はネストしたコントローラのケースに限らず、FXML全般で使える。)


なお、initializeメソッドで受け取るリソースバンドルは、fx:includeタグのresources属性で指定可能であり、省略した場合は親のリソースバンドルが、そのまま子にも渡ってくる。

ただし、includeタグで指定したリソースの場合は内部的には

resources = ResourceBundle.getBundle(childResourceName, Locale.getDefault(),
    parentResources.getClass().getClassLoader());

と同等な処理で取得されるため、取得方法が柔軟ではないことに注意が必要である。


親のFXML

ネストしたコントローラを使うFXMLは、単に「fx:include」を使うだけである。

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>

<VBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
   <children>
      <GridPane hgap="3.0" vgap="3.0">
        <columnConstraints>
          <ColumnConstraints hgrow="SOMETIMES" percentWidth="0.0" />
          <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
            <ColumnConstraints hgrow="SOMETIMES" percentWidth="0.0" />
        </columnConstraints>
        <rowConstraints>
          <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
          <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
          <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
        </rowConstraints>
         <children>
            <Label text="%directory" GridPane.halignment="RIGHT">
               <GridPane.margin>
                  <Insets right="5.0" />
               </GridPane.margin>
            </Label>
            <Label text="%namePattern" GridPane.halignment="RIGHT" GridPane.rowIndex="1">
               <GridPane.margin>
                  <Insets right="5.0" />
               </GridPane.margin>
            </Label>
            <fx:include fx:id="dirTextField" source="DirTextField.fxml" GridPane.columnIndex="1" />
            <TextField fx:id="txtNamePattern" text="*.*" GridPane.columnIndex="1" GridPane.rowIndex="1" />
            <CheckBox fx:id="chkSubdir" mnemonicParsing="false" text="%includeSubDirs" GridPane.columnIndex="1" GridPane.rowIndex="2" />
         </children>
      </GridPane>
      <FlowPane alignment="TOP_RIGHT" hgap="5.0" vgap="5.0">
         <children>
            <Button fx:id="btnOK" defaultButton="true" mnemonicParsing="false" onAction="#onOK" text="%ok" />
            <Button cancelButton="true" mnemonicParsing="false" onAction="#onCancel" text="%cancel" />
         </children>
         <padding>
            <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
         </padding>
      </FlowPane>
   </children>
   <padding>
      <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
   </padding>
</VBox>

この中の「<fx:include fx:id="dirTextField" source=.../>」が、ネストしたコントローラである。

f:id:seraphy:20150411141331p:image

カスタムコントロールとほとんど同じであるが、SceneBuilder2で開くと、インポートなどの事前処理をせずとも普通にFXMLを読み込めるので、それがBorderPaneであることがわかるようになっている。


(なお、現状ではSceneBuilder2からはfx:includeを扱うのはめんどくさいので、FXMLを手作業で直したほうが簡単である。)


親コントローラ

親コントローラでは、子コントローラを受け取るために以下のような記述ができる。

    /**
     * 入れ子になったFXMLのルートがパイントされる.
     * フィールド名は子FXMLのidと同じでなければならない.
     */
    @FXML
    private BorderPane dirTextField;

    /**
     * 入れ子になったFXMLでインスタンス化されたコントローラがバインドされる
     * フィールド名は"子FXMLのid + Controller"でなければならない.
     */
    @FXML
    private DirTextFieldController dirTextFieldController;

"ネストしたコントローラのid"と同じフィールド名によって、そのコンポーネントを得ることができ、

"ネストしたコントローラのid + Controller"というフィールド名によって、その子コントローラを得ることができる。


ここでは親FXMLのロード方法は変更する必要はない。


実行結果

実行結果としては、カスタムコンポーネントもネストしたコントローラも同じである。


なので、画面キャプチャは省略する。


カスタムコントロールとネストしたコントローラの使い分けとしては、

ということではないか、と考えられる。


CSSの適用

FXMLはUIコンポーネントの構造を表現しており、スタイルはcssで表現することができるようになっている。

http://docs.oracle.com/javafx/2/api/javafx/scene/doc-files/cssref.html


CSSといっても、FXML自体にレイアウトペイン各種が存在しているため、CSSでレイアウトを決めるということはない。

CSSではフォントや前景色、背景色などの、こまかな調整を行うために使うことができる。


同様なことはFXML上のインラインスタイルでも指定可能であるが、パフォーマンス的にはcssのほうが良いようである。*6



CSSではあるが、記述ルールがCSSというだけであり、HTMLなどで使われるウェブのCSSとは何の関係もない。


すべての属性は「-fx-」のベンダプレフィックスにより始まっており、使われているプロパティ名も、基本的にはJavaFXコンポーネントのプロパティ名を元にしている、とのことである。*7

たとえば、ボタンテキストの前景色(いわゆるフォントの色)を指定するのには「-fx-text-fill」という属性になる。

これはHTMLとは何の関係もなく、textFillプロパティ側からの命名である。


また、FXMLのCSSは、使えるクラス名としては、

  • .root → ルート(すべてに適用)
  • .クラス名を小文字化・ハイフン区切りにしたもの → そのクラスのコンポーネントに適用
    • Buttonクラスならば、.button
    • ToggleButtonクラスならば、.toggle-button
  • FXML側でUI要素ごとに任意のクラス名を追加指定することも可能
  • ボタンなどはhoverなどの擬似セレクタも使用可能

などの既定のルールがある。



使用例

以下のCSSをFXMLと同じディレクトリ上の「SimpleApp5.css」という名前で保存しておく。


また、同じディレクトリ上に背景画像用の「background-image.jpg」を置いておく。*8

.root {
    -fx-background-color: #ffff80;
    -fx-font-size: 12pt;
    -fx-background-image: url("background-image.jpg");
    -fx-background-repeat: stretch;
    -fx-background-size: 800 400;
    -fx-background-position: right bottom;
}

.button {
    -fx-background-color: #8888ff;
    -fx-text-fill: #ffff88;
    -fx-background-insets: 2;
    -fx-background-radius: 10;
    -fx-padding: 3 20 3 20;
}

.text-field {
    -fx-background-insets: 3;
    -fx-background-color: #eeeeee;
}

.label {
    -fx-text-fill: black;
}

.button:hover {
    -fx-background-color: #80f0ff;
}

FXML側では"@"による相対位置指定で、このCSSリソースを相対参照するように設定する。

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>

<VBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity"
	xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"
	stylesheets="@SimpleApp5.css">
...(中略)...
</VBox>

内部でfx:includeされているネストされたコントローラは暗黙で親のCSSを引き継ぐので、同じCSSが適用される。


CSSはプログラム的に切り替えることも可能である。


実行結果

f:id:seraphy:20150411141332p:image

このようにCSSによってレイアウトや見た目の細かな調整ができるようになっている。


これらについてはコードは不要であり、デザインとロジックが分離されているため、コードで書くよりも何をしているのか、あとから見てもわかりやすいといえる。


※ 問題は、cssで使える属性が何か?を把握することであるが、Java Day Tokyo 2015で聞いてきたテクニックとして、

Scene BuilderのViewメニューから「Show CSS Analyzer」を選択すると、適用可能な属性一覧と、現在選択している要素の属性のソースがどこからきているか、などをチェックできるので、これを使うと便利とのことである。

(ただし、Scene Builder側からはcssを編集することはできない。確認には、そこそこ便利に使える。)



Antによる実行可能jarの作成と、JavaFXのランタイムを含む実行イメージの作成方法

Java8には"ant-javafx.jar"というant用のプラグインが同梱されており、

JavaFXをjar化するためのタスクを使えるようになっている。

実行可能jarの作成

JavaFX8は通常の実行可能jarとしてビルドできるが、せっかくなので、ant-javafxプラグインを使ってビルドしてみる。


以下、build.xmlを示す。

<?xml version="1.0" encoding="UTF-8"?>
<project name="project" default="default" basedir="."
    xmlns:fx="javafx:com.sun.javafx.tools.ant">

    <!-- ビルド情報 -->
    <description>SimpleApp5のビルド</description>
    <property name="appName" value="SimpleApp5"/>
    <property name="mainClass" value="jp.seraphyware.simpleapp5.SimpleApp5"/>
    <property name="vendorName" value="seraphyware.jp"/>

    <!-- Jdk8のjavapackagerのantタスクを定義する -->
    <taskdef resource="com/sun/javafx/tools/ant/antlib.xml"
        uri="javafx:com.sun.javafx.tools.ant"
        classpath=".:${java.home}/../lib/ant-javafx.jar" />

    <!-- アプリケーション情報 -->
    <fx:application
        id="appid"
        name="${appName}"
        mainClass="${mainClass}"/>

    <!-- 実行可能jarとしてビルドする -->
    <target name="default" description="ビルドと実行可能jarの作成">
        <delete dir="work"/>
        <mkdir dir="work"/>
        <javac srcdir="src" destdir="work"
            includeantruntime="false">
            <classpath>
                <pathelement path="work"/>
            </classpath>
        </javac>

        <fx:jar destfile="${appName}">
            <fx:application refid="appid"/>

            <fileset dir="work"/>
            <fileset dir="src" excludes="**/*.java"/>

            <manifest>
                <attribute name="Implementation-Vendor" value="${vendorName}"/>
                <attribute name="Implementation-Version" value="1.0"/>
            </manifest>
        </fx:jar>
        <delete dir="work"/>
    </target>
</project>

このant-javafx.jarは、JDK8の中に含まれているが、Antから暗黙では参照できないので「${java.home}/../lib/ant-javafx.jar」のように指定している。


JREを含む実行可能イメージの作成

このant-javafxプラグインには他にも様々な機能があり、JREを含む"自己完結型アプリケーションのパッケージ化"が可能になっている。

たとえば、fx:deployタスクを使い、nativeBundler="all"と設定すると、

  • Windowsであれば
    • JREを含む実行可能イメージ
    • Inno Setupが利用可能であれば、exeによるインストーラ
    • WiXが利用可能であれば、msiによるインストーラ
  • MacであればJREを含む実行可能イメージと、それをdmgにしたものと, pkgによるインストーラ
  • Linuxであれば実行可能イメージとrpmまたはdeb

が作成される。


※ それぞれの環境でビルドする必要がある。(dmg,exe,debが欲しいならば3つのosでビルドしなければならない。)


以下はJREを含む実行可能イメージの作成用スクリプトである。

    <!-- バンドルの作成 -->
    <target name="makeBundles" depends="default"
        description="自己完結型アプリケーションのパッケージ化">
        <delete dir="dist"/>
        <mkdir dir="dist"/>

        <!-- JREを含む実行イメージの作成.(imageタイプ)

         nativeBundles="all"にすると、現在のビルド時の環境で作成できる、
         すべてのタイプのインストーラを含む配布パッケージを作成する.

         * WindowsならInnoSetupがあればexe形式、WiXがあればmsi形式を作成する.
         * Macの場合はdmgとpkgを作成する.
         * Linuxの場合はdebとrpmを作成する.
         -->
        <fx:deploy
            width="800"
            height="600"
            nativeBundles="image"
            outdir="dist"
            outfile="${appName}">

            <fx:application refid="appid"/>

            <fx:resources>
                <fx:fileset dir="." includes="*.jar"/>
            </fx:resources>

            <fx:info
                title="${appName}"
                vendor="${vendorName}"/>
        </fx:deploy>
    </target>

これをWindowsで実行すると、以下のようなフォルダが得られる。

dist
└─bundles
    └─SimpleApp5
        │  msvcp100.dll
        │  msvcr100.dll
        │  packager.dll
        │  SimpleApp5.exe
        │  SimpleApp5.ico
        │  
        ├─app
        │      SimpleApp5.cfg
        │      SimpleApp5.jar
        │      
        └─runtime
            │  COPYRIGHT
            │  LICENSE
            │  README.txt
            │  THIRDPARTYLICENSEREADME-JAVAFX.txt
            │  THIRDPARTYLICENSEREADME.txt
            │  Welcome.html
            │  
            ├─bin
            │  │  attach.dll
            │  │  awt.dll
            │  │  bci.dll
            │  │  dcpr.dll
            │  │  decora_sse.dll
            │  │  deploy.dll
            │  │  dt_shmem.dll
            │  │  dt_socket.dll
            │  │  fontmanager.dll
            │  │  fxplugins.dll
            │  │  glass.dll
            │  │  glib-lite.dll
            │  │  gstreamer-lite.dll
            │  │  hprof.dll
            │  │  instrument.dll
            │  │  j2pcsc.dll
            │  │  j2pkcs11.dll
            │  │  jaas_nt.dll
            │  │  java.dll
            │  │  JavaAccessBridge-64.dll
            │  │  javafx_font.dll
            │  │  javafx_font_t2k.dll
            │  │  javafx_iio.dll
            │  │  java_crw_demo.dll
            │  │  jawt.dll
            │  │  JAWTAccessBridge-64.dll
            │  │  jdwp.dll
            │  │  jfr.dll
            │  │  jfxmedia.dll
            │  │  jfxwebkit.dll
            │  │  jli.dll
            │  │  jpeg.dll
            │  │  jsdt.dll
            │  │  jsound.dll
            │  │  jsoundds.dll
            │  │  kcms.dll
            │  │  lcms.dll
            │  │  management.dll
            │  │  mlib_image.dll
            │  │  msvcr100.dll
            │  │  net.dll
            │  │  nio.dll
            │  │  npt.dll
            │  │  prism_common.dll
            │  │  prism_d3d.dll
            │  │  prism_es2.dll
            │  │  prism_sw.dll
            │  │  resource.dll
            │  │  sawindbg.dll
            │  │  splashscreen.dll
            │  │  sunec.dll
            │  │  sunmscapi.dll
            │  │  t2k.dll
            │  │  unpack.dll
            │  │  verify.dll
            │  │  w2k_lsa_auth.dll
            │  │  WindowsAccessBridge-64.dll
            │  │  zip.dll
            │  │  
            │  ├─plugin2
            │  │      msvcr100.dll
            │  │      npjp2.dll
            │  │      
            │  └─server
            │          classes.jsa
            │          jvm.dll
            │          Xusage.txt
            │          
            └─lib
                │  accessibility.properties
                │  calendars.properties
                │  charsets.jar
                │  classlist
                │  content-types.properties
                │  currency.data
                │  flavormap.properties
                │  fontconfig.bfc
                │  fontconfig.properties.src
                │  hijrah-config-umalqura.properties
                │  javafx.properties
                │  javaws.jar
                │  jce.jar
                │  jfr.jar
                │  jfxswt.jar
                │  jsse.jar
                │  jvm.hprof.txt
                │  logging.properties
                │  management-agent.jar
                │  meta-index
                │  net.properties
                │  plugin.jar
                │  psfont.properties.ja
                │  psfontj2d.properties
                │  resources.jar
                │  rt.jar
                │  sound.properties
                │  tzdb.dat
                │  tzmappings
                │  
                ├─amd64
                │      jvm.cfg
                │      
                ├─cmm
                │      CIEXYZ.pf
                │      GRAY.pf
                │      LINEAR_RGB.pf
                │      PYCC.pf
                │      sRGB.pf
                │      
                ├─ext
                │      access-bridge-64.jar
                │      cldrdata.jar
                │      dnsns.jar
                │      jaccess.jar
                │      jfxrt.jar
                │      localedata.jar
                │      meta-index
                │      nashorn.jar
                │      sunec.jar
                │      sunjce_provider.jar
                │      sunmscapi.jar
                │      sunpkcs11.jar
                │      zipfs.jar
                │      
                ├─fonts
                │      LucidaBrightDemiBold.ttf
                │      LucidaBrightDemiItalic.ttf
                │      LucidaBrightItalic.ttf
                │      LucidaBrightRegular.ttf
                │      LucidaSansDemiBold.ttf
                │      LucidaSansRegular.ttf
                │      LucidaTypewriterBold.ttf
                │      LucidaTypewriterRegular.ttf
                │      
                ├─images
                │  └─cursors
                │          cursors.properties
                │          invalid32x32.gif
                │          win32_CopyDrop32x32.gif
                │          win32_CopyNoDrop32x32.gif
                │          win32_LinkDrop32x32.gif
                │          win32_LinkNoDrop32x32.gif
                │          win32_MoveDrop32x32.gif
                │          win32_MoveNoDrop32x32.gif
                │          
                ├─jfr
                │      default.jfc
                │      profile.jfc
                │      
                ├─management
                │      jmxremote.access
                │      jmxremote.password.template
                │      management.properties
                │      snmp.acl.template
                │      
                └─security
                        blacklist
                        blacklisted.certs
                        cacerts
                        java.policy
                        java.security
                        javaws.policy
                        local_policy.jar
                        trusted.libraries
                        US_export_policy.jar

SimpleApp5.exeという、本物の実行可能ファイルが作成されており、

従来であれば、launch4jなどのサードパーティのツールを使ってexe化し配布用パッケージを作成していたものが、標準の方法だけでexe化することができる。

また、これは配下のruntime下にコピーされているJREのサブセットを利用して実行されるように設定されている。


したがって、この「dist\bundles\SimapleApp5」以下のフォルダごと圧縮して他のマシンに持って行けば、マシンに既にJava8u40がインストールされているか問わず、そのまま利用可能となる。


※ なお、これはJREをまるごとコピーしたものではなく、ファイルをみると実行に必要のないツール類(*.exe)は含まれていない。

※ なお、JNLPを使ったウェブ配備用のファイルも作成されるが、今回は使う予定はないので無視している*9


これはWindowsの例であるが、同様にMacで実行すると、やはりMacで実行するためのアプリケーションバンドルが作成される。

とくにMacの場合はアプリケーションバンドルにすることでスクリーンメニュー上のアプリケーション名も、きちんと設定されるようになる、という重要なメリットがある。


結論

使ってみたところ、とくには嵌る点もなく、案外使いやすい。

複雑なものについては、まだ試していないので分からないのだが、簡単に使う分には、気軽に使える程度のものであることは分かった。


Scene Builderによるグラフィカルな編集ができるのは、やはり大きなメリットだと思う。


また、Scene Builderを使わずとも、FXML自体もわかりやすいので、パッと見て、どんな画面になるのか想像できる。

Swingのようにコードを読みながら画面がどう構成されるのかを考える難しさは軽減されており、レイアウトまわりの見通しが大変良いため、画面改修時の保守性が高いことは間違いないと思われる。


なので、とりあえず、今後はJavaFXの習得を兼ねて、簡単なところからJavaFXを使ってみたいと思う。


今回のコードは、GitHubに置いてあります。


今までに判明している注意点
  1. JavaFX8に対応していない環境だと画面が激しく乱れる。(サポートされていないグラボがあるらしい?)
    • MacBook2007LateのLion上に入れたJdk8で発生。Sceneがぐちゃぐちゃになり何も見えない。
  2. 動きがおかしなところがあったりする。たとえば、とても長いテーブルのホイールスクロールとか、かなり怪しい。(使い方が悪いのかも?)
  3. JavaFX標準のチャートのカスタマイズは大変難しい。(cssを使わざるを得ず、且つ、それが大変めんどくさい。)
  4. JogAmpや、ImageIOといった類がSwingやBufferedImageを想定しており、JavaFXでは直接使えないので、いろいろ苦労しそう。
  5. バージョンによって動きが変わることがある。バグが結構残存している感があり、updateのたびに改善さ

*1http://www.oracle.com/technetwork/jp/java/javafx/overview/faq-1446554-ja.html#6

*2:JDT2015の櫻庭さんのセッションによると http://gluonhq.com/gluon-supports-scene-builder/ からビルド済みのものが取得できるらしい?

*3:JDT2015の櫻庭さんのセッションによると、Eclipse NetBeans等があればSceneBuilderはビルドできるらしい。

*4:初期化のためにをオーバーライドできる。

*5:自分でソースからビルドしたScene Builderの場合は連携がうまくゆかない場合もあるらしい。

*6:JDT2015で聞いてきたところによると

*7:JDT2015で聞いてきたところによると

*8:画像は、こちらのフリー素材からいただいたものをサイズ加工しています。 http://flowerillust.com/html/orange/orange1001.html

*9:いらないので作成したくないのだが、どうやっても必ず作成されるようだ。

2014-05-04 ForkJoinPoolとForkJoinTaskの使い方とブロッキングの実装方法

[] ForkJoinPoolとForkJoinTaskの特徴と使い方  ForkJoinPoolとForkJoinTaskの特徴と使い方を含むブックマーク

Fork/Joinとは?

JavaSE7でサポートされるjava.util.concurrent.ForkJoinPoolは、ExecutorServiceの一員であり、一見するとThreadPoolExecutorに似たようなものに思えるが、実際は全く別の、異質のものである。


ForkJoinPoolは、ありていに言えばWork-stealingアルゴリズムの実装であり、無数のタスクを無駄なく論理CPU分のスレッドに割り当てるものである。

ForkJoinPoolはデフォルトでは論理CPU数分のスレッドプールをもつように構築される。

そして、あるタスクが完了するか、もしくは待ちになったら、すぐに別のタスクがアクティブとなり、常に、論理CPU数分のスレッドだけがアクティブになるように調整される。


また、ForkJoinPoolに入れたForkJoinTaskは、そのタスクの中で新たなタスクをforkすると、同じForkJoinPool内にタスクを予約する。

このとき、タスクは既定ではStackのようにタスクが積まれるため、再帰的な計算をさせるのに向いている。


ForkJoinTaskのタスクは他のタスクと連携しあっており、タスク内でforkした子タスクをjoinして結果を待つ場合には、そのスレッドは「待ち」に入るのではなく、タスクが「中断」された状態となり、スレッドは他のタスクの処理に回される(Work-stealing)ようになっている。

(つまり、常に論理CPUに空きがでないようにタスクが割り当てられ、joinしても、スレッドは待ち状態にならない、ということでもある。)


このようなWork-stealing型の他の言語の処理ライブラリとしては、C/C++やFortanで広く使えるOpenMPや、MicrosoftのPPL(並列パターン ライブラリ)のようなものがある。


また、ForkJoinTaskでは、forkしてjoinする場合、最後にforkしたタスクがまだスレッドに割り当てられていなければ、join後に、そのまま同じスレッドでforkした処理を継続する、つまりスレッドを切り替える必要がない、といった最適化を施すことができるようになっているようである。



(なお、ForkJoinTask#invokeは、forkとjoinを同時に行うコンビニエンスメソッドである。)


このように、ForkJoinPoolでキモとなるのは、その名のとおり「fork」と「join」である。

とりわけjoinによってタスクが中断してスレッドが別のforkしたタスクの処理に回されるところが重要である。


実験コード

package jp.seraphyware.forkjoinsample;

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveAction;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class Main {

    /**
     * タスク内からタスクを生成する最大深さ
     */
    private static final int maxDepth = 4;

    /**
     * 実行中、もしくは待機中のタスクの数.
     */
    private static AtomicInteger activeTaskCnt = new AtomicInteger(0);

    /**
     * タスクを生成して返す.
     * @param no タスクの識別子、深さも表す
     * @return タスク
     */
    private static ForkJoinTask<?> createRecursiveTask(final String no) {
        return new RecursiveAction() {
            private static final long serialVersionUID = 1L;

            @Override
            protected void compute() {
                int numOfEntered = activeTaskCnt.incrementAndGet();
                log("[enter] activeTask=" + numOfEntered);
                try {
                    if (no.length() < maxDepth) {
                        // 最大深さに達していなければ、さらに10個の子タスクを生成する.
                        ForkJoinTask<?>[] tasks = new ForkJoinTask<?>[10];
                        for (int idx = 0; idx < tasks.length; idx++) {
                            tasks[idx] = createRecursiveTask(no + idx);
                        }
                        // 子タスクをすべて一括実行し、それを待機する.
                        // (内部的にはforkしてjoinしている)
                        invokeAll(tasks);
                    }
                } finally {
                    numOfEntered = activeTaskCnt.decrementAndGet();
                    log("[leave] activeTask=" + numOfEntered);
                }
            }

            /**
             * 診断メッセージ表示用
             * @param prefix
             */
            private void log(String prefix) {
                // 1つのスレッドの使われ方を見るために、特定のスレッドだけを診断メッセージを表示する。
                if (Thread.currentThread().toString().equals("Thread[ForkJoinPool-1-worker-1,5,main]")) {
                    StringBuilder buf = new StringBuilder();
                    buf.append(prefix);
                    buf.append(" no.").append(no);
                    buf.append(" :numOfActive=").append(getPool().getActiveThreadCount());
                    buf.append(" :poolSize=").append(getPool().getPoolSize());
                    buf.append(" :").append(Thread.currentThread());
                    System.out.println(buf.toString());
                }
            }
        };
    }

    public static void main(String[] args) throws Exception {
        // ForkJoinスレッドプール作成、デフォルトは論理CPU数
        ForkJoinPool forkJoinPool = new ForkJoinPool();

        // 最初のタスクを生成し、完了を待つ
        forkJoinPool.invoke(createRecursiveTask("0"));

        // スレッドプールの終了
        forkJoinPool.shutdown();
        forkJoinPool.awaitTermination(10, TimeUnit.SECONDS);
        System.out.println("done");
    }
}

実験結果

特定のスレッドの動きだけを確認するため、1つのスレッドのみにログを絞り込んでいる。

[enter] activeTask=1 no.0 :numOfActive=1 :poolSize=1 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=3 no.00 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=8 no.000 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=15 no.0000 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=16 no.0000 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=17 no.0001 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=18 no.0001 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=22 no.0002 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=16 no.0002 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=16 no.0003 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=17 no.0003 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=19 no.0004 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=16 no.0004 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=19 no.0005 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=17 no.0005 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=16 no.0006 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=12 no.0006 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=14 no.0007 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=13 no.0007 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=16 no.0008 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=15 no.0008 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=14 no.0009 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=15 no.0009 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=10 no.000 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=12 no.001 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=16 no.0010 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=16 no.0010 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=18 no.0011 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=16 no.0011 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=17 no.0012 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=16 no.0012 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=17 no.0013 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=17 no.0013 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=20 no.0014 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=16 no.0014 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=17 no.0015 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=16 no.0015 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=15 no.0016 :numOfActive=9 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=13 no.0016 :numOfActive=9 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=15 no.0017 :numOfActive=9 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=12 no.0017 :numOfActive=9 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=15 no.0018 :numOfActive=9 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=13 no.0018 :numOfActive=9 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=13 no.0019 :numOfActive=9 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=9 no.0019 :numOfActive=9 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=10 no.001 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[enter] activeTask=9 no.0029 :numOfActive=8 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=5 no.0029 :numOfActive=7 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=1 no.00 :numOfActive=1 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
[leave] activeTask=0 no.0 :numOfActive=1 :poolSize=8 :Thread[ForkJoinPool-1-worker-1,5,main]
done

1つのスレッドで繰り返しcomputeメソッドがenterされていることが分かるようにタスクはjoinによって中断・再開されている。


ForkJoinTaskとRecursiveTask/RecursiveActionの違い

ForkJoinPoolに入れるタスクはForkJoinTask型であるが、実際にはRecursiveTaskかRecursiveActionのいずれかから派生するのが便利である。

RecursiveTaskは戻り値のあるタスク、RecursiveActionは戻り値が無いタスクを定義するのに使う。


この2つのクラスの実装は非常に単純で、その親であるForkJoinTask<T>が戻り値のあり・なしにかかわらず処理できるように汎用的になっているため、派生クラスで用途に応じて必要な部分だけを実装すれば良いように単純化しているだけの、単純なアダプタークラスにすぎない。

そのあたりはFork/Joinのメカニズムとは直接関係ない。


ForkJoinPoolのasyncモードでの利用

ForkJoinPoolを、論理CPU数分のスレッドに空きが出ないようにタスクを割り当てるスレッドプールとして利用することもできる。


通常、ForkJoinPoolに入れるタスクはfork/joinの利用に適したようにスタック式に積み上げられるが、

コンストラクタでasyncモードを指定することでイベント処理に適したFIFO式に設定することができる。


ForkJoinPoolは、タスク内で新たにタスクをforkする必要がなければ、普通なCallable, Runnableをタスクとして入れることができ、

また、Callable, RunnableをForkJoinTaskに変換するためのForkJoinTask.adapt()メソッドを使うこともできる。


すべてのタスクがCallable, Runnableのように内部で子タスクを作ることがなければ、ForkJoinPoolはasyncモードにしてタスクの実行順序をFIFOにしたほうが都合がよい場合が多いと思われる。

package jp.seraphyware.forkjoinsample;

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;

public class Main {

    public static void main(String[] args) throws Exception {
        ForkJoinPool pool = new ForkJoinPool(
                Runtime.getRuntime().availableProcessors(),
                ForkJoinPool.defaultForkJoinWorkerThreadFactory,
                null,
                true); // タスクの積み上げにFIFOモードを使用する.(通常はSTACKモード)

        for (int idx = 0; idx < 20; idx++) {
            final int cnt = idx;
            pool.submit(new Runnable() { // 内部的にはForkJoinTask.adapt()でForkJoinTaskに変換している.
                @Override
                public void run() {
                    System.out.println("idx=" + cnt + "/"
                            + Thread.currentThread());
                    try {
                        Thread.sleep((long) (Math.random() * 100));
                    } catch (InterruptedException ex) {
                        // 何もしない
                    }
                }
            });
        }
        pool.shutdown();
        pool.awaitTermination(10, TimeUnit.SECONDS);
        System.out.println("done");
    }
}

この場合、タスクを入れた順に処理されることになる。

ただし、それぞれのタスクの完了までの時間によっては並列実行しているために順序が入れ替わることもありえる。


結論

fork/joinと同じやり方で、ThreadPoolExecutorでタスク内で自分自身のスレッドプールに新しいタスクを割り当て(submit)して、そのタスクをjoin(get等)した場合、1つのスレッドが割り当てられ、且つ、1つのスレッドが待ち状態となるため、あっという間に論理CPU数分のスレッドを使いきって、スレッドプールが枯渇すると思われる。


タスクをfork/joinという小さな粒度で分割しスレッドを効率的に使いまわす仕組みは、従来のExecutorのタスクの単位の制御とは全然違うし、従来のExecutorを直接使うのでは得られない効果である。


ForkJoinPoolを使うことで、たしかに分割統治的なアルゴリズムで並列処理させたい場合には、論理CPUの個数にかかわらず、単に再帰的な処理を記述するだけで並列処理が実現できるようになる。

ForkJoinPoolに再帰タスクを入れるだけで、スレッドの割り当てなどの面倒な処理はお任せできるようになっている。

これは自前で用意するのは、なかなか面倒なものだと思うので、Fork/Joinは大変有益なAPIであるのではないか、と思われる。


また、Java8からは新たに、CompletableFutureというタスク処理のクラスが追加されており、こちらは従来のタスク処理とForkJoinPoolの処理とを統合した、最終形態的な大量メンバを抱える多機能クラスとなっている。

CompletableFutureは、C#などで使われる継続タスク、あるいはjQueryのDeferredのような形でタスクを定義してゆくことができるようになっており、これを非同期タスクとして処理する場合は暗黙でForkJoinPoolを使うようになっているため、Java8では、ForkJoinPoolの活用の機会も増えそうである。

なお、JavaEE7からはjavax.enterprise.concurrent.ManagedExecutorServiceなどでスレッドの制御が可能になっているものの、残念なことに、ForkJoinPoolは対象外になっている。*1 *2


関連資料

[] ForkJoinPoolにおけるタスクのブロッキングの実装方法 (2014/06/03)  ForkJoinPoolにおけるタスクのブロッキングの実装方法 (2014/06/03)を含むブックマーク

ForkJoinPoolにおけるタスクのブロッキングの方法

先にForkJoinPoolの特性や使い方について調べたが、「同期処理」や、単純な「待ち」を含むタスクが、どのようになるのか調べてみた。


結論からいうと、同期をとる必要があるタスクは、Java8でサポートされるCompletableFutureを使って、タスクを細切れにして分割したり結合したりするのが、もっとも手っ取り早い。


しかし、ForkJoinPoolが持つメカニズムとしては、少し手間をかけることで自前で実装することも可能となっている。


単純にブロッキングした場合にForkJoinPoolはどうなるか?

ForkJoinPoolは、そのタスクの中でjoinすることでタスクを待ち状態にすると、ただちに空いたCPU資源を他のタスクに割り当てるようにスケジューリングする。

もちろん、スケジューラは、joinメソッドの中で、この切り替えを行っている。


では、たとえば単純に「Thread.sleep()」などでタスクを待ち状態にしたら、どうなるであろうか?


スケジューラはタスクから何も通知を受けることが無いため、実際にはスレッドは寝ているにもかかわらず、他のスレッドにCPUを割り当てることができない。

これは、synchronized/wait/notifyによるスレッドのブロックや、あるいは同期クラスを使ったスレッドのブロックも同様である。

もし、アクティブなスレッドがすべてブロックしたならば、それらのブロックが解除されるまでタスクは全く実行されなくなってしまうことになる。


したがって、ForkJoinTaskの中では、これらのスレッドを直接ブロックするような同期処理は使うべきではない。


もし、ForkJoinTaskの中でタスクのブロックが必要な場合には、ManagedBlockerを使うことができる。

これは、ブロッキングを行うべきか判定するメソッドと、実際にブロッキングを行うメソッドの2つのメソッドをもつ簡単なインターフェイスである。

ここに同期に必要な処理を記述したのちに、ForkJoinPool.managedBlock(java.util.concurrent.ForkJoinPool.ManagedBlocker)メソッドに渡すことで、ブロッキングが必要であればアクティブなタスクを切り替えることができるようにスケジューラと協調してブロッキングを行うことができるようになる。

ForkJoinTaskの中で協調的に動作するSleepの実装例
    // 任意の時間、スリープする管理されたブロッカーを構築する関数
    LongFunction<ManagedBlocker> managedSleepFactory =
            (long tim) -> {
                // 開始時間
                long st = System.currentTimeMillis();
                
                return new ManagedBlocker() {
                    @Override
                    public boolean block() throws rruptedException {
                        if (!isReleasable()) {
                            // まだ指定時間経過していなければ
                            // 10mSecスリープする.
                            // (最小分解能を10mSecとする)
                            Thread.sleep(10);

                            // まだスリープする必要があるか?
                            return isReleasable();
                        }
                        return true;
                    }

                    @Override
                    public boolean isReleasable() {
                        // 開始時間から指定時間が経過しているか?
                        long span = System.currentTimeMillis() - st;
                        return span > tim;
                    }
                };
            };
ForkJoinTaskの中で協調的に動作するSleepの使用例
    // タスクごとのループ回数
    int mxLoop = 5;

    // タスクを連続して生成する
    ForkJoinPool fjPool = ForkJoinPool.commonPool();
    List<ForkJoinTask<?>> tasks = new ArrayList<>();
    IntStream.rangeClosed(0, 14).forEach(idx -> {
        tasks.add(fjPool.submit(ForkJoinTask.adapt(()->{
            IntStream.rangeClosed(0, mxLoop).forEach(loop -> {
                // タスク内ループからログ出力する
                logWriter.accept(idx, loop);
                try {
                    // 一定時間スリープする
                    ForkJoinPool.managedBlock(managedSleepFactory.apply(200));
                    //Thread.sleep(200); // 管理されていないスリープ

                } catch (InterruptedException ex) {
                    ex.printStackTrace();
                    // 無視する.
                }
            });
        })));
    });
    
    // すべてのタスクの完了を明示的に待つ
    // (すべてのスレッドがブロックしているとタスク完了前に
    // awaitTerminationが抜けてしまうため。Java8u4で確認。)
    tasks.forEach(ForkJoinTask::join);
    
    // ForkJoinPoolを終了する。
    fjPool.shutdown();
    fjPool.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
ログ出力用ヘルパ関数
    // タスクからのログを受け取る
    AtomicInteger cols = new AtomicInteger();
    BiConsumer<Integer, Integer> logWriter = (idx, loop) -> {
        String text = String.format("[%02d] %02d%s  ",
                idx, loop, (loop == mxLoop) ? '*' : ' ');
        synchronized (cols) {
            int col = cols.incrementAndGet() % 10; // 10個ごとに改行
            if (col == 0) {
                System.out.println(text);
            } else {
                System.out.print(text);
            }
        }
    };
実行結果

上記のコードを実行すると、タスクを連続して15個作成し、

それぞれのタスクで5回ログを出しつつ、1回ごとに200mSecのSleepを行う。


このとき、ManagedBlockerによって協調的にスリープするため、スリープしている間は他のタスクがアクティブとなる。


その結果、すべてのタスクが、あたかも同時実行的に並列に進行してゆくことが確認できる。

角括弧の中はタスク番号であり、「*」マークが1タスク中の最終処理を表していて、すべてのタスクの最終処理がまとまって処理されていることがわかる。

[05] 00   [04] 00   [00] 00   [06] 00   [02] 00   [01] 00   [03] 00   [07] 00   [08] 00   [09] 00   
[10] 00   [11] 00   [12] 00   [13] 00   [14] 00   [04] 01   [02] 01   [03] 01   [01] 01   [05] 01   
[00] 01   [06] 01   [11] 01   [07] 01   [08] 01   [09] 01   [10] 01   [12] 01   [13] 01   [14] 01   
[04] 02   [00] 02   [02] 02   [03] 02   [05] 02   [01] 02   [06] 02   [11] 02   [07] 02   [08] 02   
[09] 02   [10] 02   [13] 02   [12] 02   [14] 02   [04] 03   [05] 03   [03] 03   [02] 03   [00] 03   
[01] 03   [06] 03   [11] 03   [07] 03   [09] 03   [08] 03   [10] 03   [14] 03   [12] 03   [13] 03   
[05] 04   [01] 04   [03] 04   [04] 04   [02] 04   [00] 04   [06] 04   [11] 04   [07] 04   [09] 04   
[08] 04   [10] 04   [14] 04   [13] 04   [12] 04   [03] 05*  [05] 05*  [00] 05*  [01] 05*  [04] 05*  
[02] 05*  [06] 05*  [11] 05*  [07] 05*  [08] 05*  [09] 05*  [10] 05*  [12] 05*  [13] 05*  [14] 05*  


これに対して、コメントアウトしている「Thread.sleep(200)」のほうを有効とすると、タスクの切り替えは発生しないため、処理をはじめたタスクのスリープ解除と完了を待ってから次のタスクに進む、という効率の悪い動きとなる。

また、タスクの最終処理はタスクの処理順となっている。

直列的な動作となるため、当然、トータルの所要時間は前のものよりも長くなる。

(以下は、8個の論理CPUがあるマシンの場合の動きである。)

[01] 00   [03] 00   [05] 00   [00] 00   [06] 00   [04] 00   [02] 00   [03] 01   [01] 01   [02] 01   
[05] 01   [04] 01   [00] 01   [06] 01   [01] 02   [05] 02   [02] 02   [00] 02   [03] 02   [04] 02   
[06] 02   [06] 03   [00] 03   [01] 03   [03] 03   [05] 03   [02] 03   [04] 03   [03] 04   [02] 04   
[05] 04   [06] 04   [00] 04   [01] 04   [04] 04   [03] 05*  [01] 05*  [02] 05*  [05] 05*  [00] 05*  
[06] 05*  [04] 05*  [07] 00   [10] 00   [08] 00   [09] 00   [11] 00   [12] 00   [13] 00   [10] 01   
[11] 01   [07] 01   [09] 01   [12] 01   [08] 01   [13] 01   [09] 02   [07] 02   [10] 02   [08] 02   
[12] 02   [11] 02   [13] 02   [07] 03   [11] 03   [10] 03   [09] 03   [12] 03   [08] 03   [13] 03   
[12] 04   [10] 04   [07] 04   [11] 04   [08] 04   [09] 04   [13] 04   [08] 05*  [12] 05*  [10] 05*  
[09] 05*  [07] 05*  [11] 05*  [13] 05*  [14] 00   [14] 01   [14] 02   [14] 03   [14] 04   [14] 05*  

Java8のCompletableFutureを使う

複数のタスクに処理を分解し、それぞれのタスクの実行順序を決めるのであれば、CompletableFutureをつかうと、それらのタスクの実行順序や待ち合わせなどが手軽に実装できる。

3つのタスクを順番に実行するタスクをつくり、これらのタスクを一括して待ち合わせるタスク
    // 複数個の非同期フューチャーを生成して即時実行を開始する.
    @SuppressWarnings("unchecked")
    CompletableFuture<Void>[] cfs = IntStream.rangeClosed(0, 11).oObj(idx -> {
        return CompletableFuture.supplyAsync(() -> {
                // 一段目
                String msg = String.format("[%02d]job-A", idx);
                System.out.println(msg);
                uncheckedSleep.accept(1000);
                return msg;

            }).thenApplyAsync(prevResult -> {
                // 二段目
                String msg = String.format("[%02d]job-B: %s", idx, Result);
                System.out.println(msg);
                uncheckedSleep.accept(500);
                return msg;

            }).thenApplyAsync(prevResult -> {
                // 三段目
                String msg = String.format("[%02d]job-C: %s", idx, Result);
                System.out.println(msg);
                uncheckedSleep.accept(800);
                return msg;

            }).thenAcceptAsync(System.err::println);
        }).toArray(len -> new CompletableFuture[len]);

    // すべてのフューチャーの完了を待ち合わせる
    CompletableFuture<Void> jobs = CompletableFuture.allOf(cfs);
    System.out.println("waiting...");
    jobs.join();
    System.out.println("done!");

なお、CompletableFutureの非同期タスク(*async)系メソッドは、暗黙でForkJoinPoolの既定のプールを使用するので、上記コードは裏ではタスクはForkJoinPool上で動いている。


スリープ関数のヘルパ

スリープが検査例外を出すとラムダ式が面倒なことになるので、ちょっとヘルパを噛ませておく。

    // 検査例外を出さないスリープ
    LongConsumer uncheckedSleep = tm -> {
        try {
            ForkJoinPool.managedBlock(managedSleepFactory.apply(tm));
            //Thread.sleep(tm); // 管理されていないスリープ
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
    };
実行結果

waiting...
[06]job-A
[04]job-A
[00]job-A
[01]job-A
[05]job-A
[02]job-A
[03]job-A
[07]job-A
[08]job-A
[09]job-A
[10]job-A
[11]job-A
[06]job-B: [06]job-A
[01]job-B: [01]job-A
[11]job-B: [11]job-A
[02]job-B: [02]job-A
[09]job-B: [09]job-A
[08]job-B: [08]job-A
[03]job-B: [03]job-A
[04]job-B: [04]job-A
[05]job-B: [05]job-A
[07]job-B: [07]job-A
[10]job-B: [10]job-A
[00]job-B: [00]job-A
[11]job-C: [11]job-B: [11]job-A
[06]job-C: [06]job-B: [06]job-A
[01]job-C: [01]job-B: [01]job-A
[03]job-C: [03]job-B: [03]job-A
[07]job-C: [07]job-B: [07]job-A
[02]job-C: [02]job-B: [02]job-A
[08]job-C: [08]job-B: [08]job-A
[09]job-C: [09]job-B: [09]job-A
[00]job-C: [00]job-B: [00]job-A
[10]job-C: [10]job-B: [10]job-A
[04]job-C: [04]job-B: [04]job-A
[05]job-C: [05]job-B: [05]job-A
[07]job-C: [07]job-B: [07]job-A
[08]job-C: [08]job-B: [08]job-A
[02]job-C: [02]job-B: [02]job-A
[06]job-C: [06]job-B: [06]job-A
[01]job-C: [01]job-B: [01]job-A
[03]job-C: [03]job-B: [03]job-A
[11]job-C: [11]job-B: [11]job-A
[00]job-C: [00]job-B: [00]job-A
[10]job-C: [10]job-B: [10]job-A
[05]job-C: [05]job-B: [05]job-A
[04]job-C: [04]job-B: [04]job-A
[09]job-C: [09]job-B: [09]job-A
done!

タスクが順序よく実行されていることが確認できる。

まとめ

  • ForkJoinTaskの中では直接ブロッキングするメソッドは使わない
  • ブロッキングが必要となる場合は ManagedBlocker を実装する
    • ただし、Java8で CompletableFuture を使えるなら、そのほうが簡単である。

以上、メモ終了

*1:fork/joinの細かなタスクの切り替え処理が、JavaEEのスレッドコンテキストまわりの調整とかで難しかった、とか、そうゆう理由だろうか?

*2JavaEEで、CompletableFutureを使う場合は、ForkJoinPool以外のExecutorを明示的に渡す必要がある、とのこと。

2014-04-19 Visual Studio搭載のT4テンプレートエンジンの3通りの活用方法

[][] Visual Studio 2013 Expressで標準サポートされるテンプレートエンジン(T4)の活用方法  Visual Studio 2013 Expressで標準サポートされるテンプレートエンジン(T4)の活用方法を含むブックマーク

コードジェネレータの有用性とT4

業務系のアプリケーションを作成していると、多かれ少なかれデータモデルのようなものをソースコード上のエンティティクラスとして表すために似たようなコードをたくさん書かなければならないことがある。

もちろん手で地道に書くなんてことはなくて、このようなものはコードジェネレータで機械的に生成させるのが良い。

だいたいデータモデルは開発中にどんどん変わってゆくのでエンティティクラスも都度変更してゆく必要があるからだ。

自動化しておけば何度でも生成できるし、コードの修正漏れなんてこともありえない。

これは良くあるシチュエーションなので、もう過去に何度も何度も何度もジェネレータを作成してきた。


ところが昨年達人出版会で興味をそそられた本を買ってきたところ、

「メタプログラミング.NET」

http://tatsu-zine.com/books/metaprogramming-dotnet

Visual Studioには、"T4"という、まさにコードジェネレータとして使うに最適なテンプレートエンジンがあらかじめ備わっている

ということを知った。


少し時間がかかってしまったが、T4の使い方について、だいたいの感触をつかめたので、記録に残しておきたいと思う。


T4とは何か?

T4とは、"Text Template Transformation Toolkit"の略であり、

その名の通り、テキストのひな形から何かしらのテキストを生成するためのツールである。

目的としては、"DSL(ドメイン特化言語)"を作るようなものと考えてよい。


先の本によれば、T4は、もともと2004年にマイクロソフトのDSLチームがASP.NETのエンジンを使って、"柔軟で簡単に統合できるコードジェネレータ"として作成したのが起源という。

そのため、テンプレートの記述形式は現在でもASP.NET的な構文の面影を残している。(現在はASP.NETとは直接関係ない。)


T4には利用形態がいくつかあるが、具体例としては、

たとえば、以下のような「デザイン時テンプレート」を作成したとする。

<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".cs" #>
<#
    var className = "MyClass";
    var properties = new String[] { "Field1", "Field2", "Field3", "Field4" };
#>
namespace TemplateSample
{
    public class <#= className #>
    {
<#
        foreach (string prop in properties)
        {
#>
        public string <#= prop #> { get; set; }
<#
        }
#>
    }
}

Visual Studio上で、このテンプレートを作成すると、自動的に以下のようなコードが生成される。

namespace TemplateSample
{
    public class MyClass
    {
        public string Field1 { get; set; }
        public string Field2 { get; set; }
        public string Field3 { get; set; }
        public string Field4 { get; set; }
    }
}

テンプレートの拡張子として「.cs」を指定しているので、生成されたテキストファイルはソースコードとみなされる。

当然、プロジェクトの他のソースコードと何ら扱いはかわりない。

もちろん、ソースコードになっているので生成されたコードのインテリセンスも効く。


このジェネレータによるソースコード生成からコンパイルまでの一連の流れは、Visual Studioに統合されておりシームレスに利用できるため、下手に自作したジェネレータを使うよりも、よっぽど使い勝手が良い。


そのためであろうか、T4は、ASP.NET MVC, ADO.NET Entity Framework、そのほか、ポピュラーなフレームワークのコード生成に利用されたため、T4は、もっとも広く使われているツール構築フレームワークの一つとなっている、という。


このT4がVisual Studioに搭載されるようになったのは、Visual Studio 2008以降(要DSLツールキット)で、Visual Studio 2010以降では標準装備となっている。

現在のVisual Studio 2013であれば無償のExpress版を含めて、すべてのエディションでT4は利用可能である。


T4の利用形態

T4には利用形態が3種類ほどある。

  • デザイン時テンプレート
  • 実行時テンプレート
  • 実行時のT4エンジンの直接利用(やや特殊)

以下、3種類の使い方と特性を示した後、それぞれの具体的な実装例についてみてゆくものとする。


デザイン時テンプレートとしての利用

テンプレートを(コンパイル前に)ファイルとして生成するのが、デザイン時テンプレートである。

先の例でみたとおり、ソースコードを自動生成するような用途に用いることができる。

(もちろん、テキストファイルであれば何でも良い。)


ソースコードへの変換はコンパイル前に行われ、コンパイル後はT4で生成されたかどうかは問わないので、生成されたアプリケーションにT4ランタイムが必要ない。


また、生成するコードなどの情報を取得するためにVisual Studio側(環境)の情報を取得するための方法が用意されている。

プロジェクト中の定義ファイルを読み取ってソースコードを自動生成する、などの用途ではプロジェクトのフォルダ位置などを判定するために、Visual Studioの環境にアクセスすることができるようになっている。


その他にも、Visual Studioからテンプレートへ引数を与え、テンプレートを制御する仕組みもある。

(ただし、このテンプレートへの引数渡しの機能を使うためには、Visual Studioのアセンブリへの参照が必要となり、且つ、Visual Studio SDKを必要とするため、実質的にProfessional以降の有償のVisual Studioでなければならない。どちらかというと、Visual Studioの拡張としてのT4の利用方法と思われる。)


実行時テンプレートとしての利用

もう一方の利用形態である「実行時テンプレート」では、Visual Studio上でテンプレートを作成すると自動的にソースコードに変換される。


ただし、デザイン時テンプレートと異なるのは、実行時テンプレートが作成するソースコードは、テンプレートそのものをソースコードに変換する「テンプレートを適用したテキストを生成するためのコードを生成するもの」である。

生成されたテンプレートクラスはT4に依存しない単純なソースとして生成される。(どんなコードが生成されたのかはファイルを開けば確認できる。)


実際にテンプレートを適用したテキストを取得するには、生成されたテンプレートクラスにデータを与えて実行することで変換・取得する。

用途としては、たとえば「アプリケーションの機能として、テキストで記述されたレポートファイルを生成しなければならない」というような場合に、そのテンプレートエンジンとして利用する、などが考えられるだろう。


実行時のT4エンジンの直接利用

「実行時テンプレート」と似ているが、実行時テンプレートではテンプレートがソースコードに変換されコンパイルされるため、テンプレートそのものを実行時に変更することはできない。

これに対してT4エンジンを直接利用すれば、実行時にテンプレートを変更できるようになる。


ただし、当然のことながら、作成されたアプリケーションはT4に依存し、T4を動かすために必要なアセンブリも実行時に必要となる。

これにはVisual Studio SDKでインストールされるアセンブリも含まれ、(ビルドサーバへの配備以外に)それらのアセンブリを再配布することは基本的にはできない。

また、Visual Studio ExpressにはVisual Studio SDKをインストールすることはできないため、Express版でもT4エンジンの直接利用はできないことになる。

よって、この方法を通常のアプリで採用するメリットはあまりないと思われる。


ただし、MonoDevelopの成果物として、T4エンジンとソースコードレベルでほぼ互換性のあるMono.TextTemplatingがあり、こちらを使うことでT4エンジン相当のものをアプリケーションに組み込むことが可能となっている。

Mono.TextTemplatingのライセンスはMIT/X11であり、非常に緩いため、商用・非商用にかかわらず大部分のケースでアプリケーションに組み込むうえでのライセンス的な支障はないと思われる。


デザイン時テンプレートの使用

まずは、デザイン時テンプレートの使い方について見てみる。

デザイン時テンプレートの作成

Visual Studio Express 2013 For Desktopで「デザイン時テンプレート」を新規に作成するには、「新しい項目の追加」から、「テキストテンプレート」を選択する。


f:id:seraphy:20140419122834p:image


このときに指定したファイル名「TextTemplate1.tt」をベースとして、

ソースコードである「TextTemplate1.txt」が自動的に生成されるようになる。


Visual Studioが、この*.ttファイルを認識して自動的にコード生成するのは、ソリューションエクスプローラから*.ttファイルのプロパティを見ると、カスタムツールが「TextTemplatingFileGenerator」となっており、これによって、このファイルがT4のテンプレートファイルであると認識してコードを生成するようになっている。

(「TextTemplatingFilePreprocessor」となっているし実行時テンプレートとして認識される。)


f:id:seraphy:20140419122835p:image


初期状態ではテンプレートは空であり、また拡張子もtxtとなっているので、ただの空のテキストファイルが生成されるテンプレートになっている。


テンプレートに与えるデータファイルの作成

デザイン時テンプレートを使う場合には、テンプレートに与えるデータをテンプレート側から取得する方法を考えなければならない。

ここでは、プロジェクト中に生成すべきクラスを定義したXMLファイルを置いておき、テンプレートによって自動的に必要なクラスを大量生産するケースを想定してみる。

<?xml version="1.0" encoding="UTF-8"?>
<classes>
    <class name="Class1">
        <field name="val1" type="System.Int32"/>
        <field name="val2" type="System.String"/>
    </class>

    <class name="Class2">
        <field name="val2" type="System.String"/>
        <field name="val3" type="System.Int32"/>
        <field name="val4" type="System.String"/>
    </class>

    <class name="Class3">
        <field name="val3" type="System.Int32"/>
    </class>

    <class name="Class4">
        <field name="val4" type="System.String"/>
        <field name="val5" type="System.Int32"/>
    </class>
</classes>

Class1からClass4まで、単純なプロパティをもつクラスを自動的に生成するものとする。


Visual Studio環境へのアクセス

このXMLファイルを「defClasses.xml」としてプロジェクト中に置いた場合、これをテンプレートからアクセスするには以下のように記述する。

<#@ template debug="true" hostspecific="true" language="C#" #>
<#@ output encoding="UTF-8" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="System.Xml" #>
<#@ assembly name="System.Xml.Linq" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Xml.Linq" #>

namespace TemplateSample
{
<#
    // templateディレクティブでhostspecific="true"とすると、
    // Visual Studioの環境にアクセスできる.
    // ResolvePathで、このテンプレートファイルからの相対位置にあるファイルパスを
    // 取得することができる.
    string file = this.Host.ResolvePath("defClasses.xml");

    XDocument xdoc = XDocument.Load(file);
    System.Diagnostics.Debug.WriteLine(xdoc);
    var classes = from classesElm in xdoc.Descendants("class")
                  select new {
                        Name = classesElm.Attribute("name").Value,
                        Fields = classesElm.Descendants("field")
                  };

    foreach (var classDef in classes) {
        var classMetaInf = new ClassMetaInf()
            {
                ClassName = classDef.Name
            };
        foreach (var field in classDef.Fields) {
            classMetaInf.Fields.Add(new FieldMetaInf()
                {
                    FieldName = field.Attribute("name").Value,
                    FieldType = field.Attribute("type").Value 
                });
        }
#>
<# // T4ではテンプレートが大きくなりすぎないように分割しincludeによって読み込むことができる #>
<#@ include file="DesigntimeTemplate2.ttinclude" #>
<#
    }
#>
}
<#+
    // クラス機能ブロック以降にはサポートクラスやメソッド等を宣言可能

    class FieldMetaInf
    {
        public string FieldName { get; set; }

        public string FieldType { get; set; }
    }

    class ClassMetaInf
    {
        public string ClassName { get; set; }

        private List<FieldMetaInf> _fields = new List<FieldMetaInf>();

        public List<FieldMetaInf> Fields
        {
            get
            {
                return _fields;
            }
        }

        public int NumberOfColumns
        {
            get
            {
                return _fields.Count;
            }
        }

        public string ColumnName(int idx)
        {
            return _fields[idx].FieldName;
        }

        public string ColumnType(int idx)
        {
            return _fields[idx].FieldType;
        }
    }
#>

templateディレクティブで、hostspecific="true" と指定する。

これにより、テンプレート中で「Host」というITextTemplatingEngineHostインスタンスを示す変数にアクセスできるようになる。

Visual Studioからテンプレートが実行される場合は、HostはVisual Studioの環境を示している。*1

    string file = this.Host.ResolvePath("defClasses.xml");

とすることで、"defClasses.xml"のファイルへのパスを得ることができる。

これによりテンプレートの内から、プロジェクトに必要な生成すべきクラスの情報を取得できることになる。


なお、ITextTemplatingEngineHostが、具体的にどのような役割で、どのようなことをしているのかは、本記事で後述する、T4テンプレートエンジンの直接利用の際に実際にホストを実装する事例がある。


サポートクラスの定義と、アセンブリの指定

テンプレートファイルには<#+ 〜 #>というブロックで囲むことで、クラス機能ブロックをつくることができ、ここにはテンプレートに必要なメソッドや、サポートクラスなどを定義することができる。


注意すべき点としては、テンプレートファイルはコンパイルされるタイミングも単位も異なっており、プロジェクト内の他のクラスは見えていない、ということである。


もし、テンプレートの中でも共有したいクラス等があれば、プロジェクトを分けるなどして別個のアセンブリとし、

テンプレートで使用するすべてのアセンブリは、アセンブリディレクティブで明示的に宣言しておく必要がある。


このとき、アセンブリディレクティブの検索方法は、GACやVisual StudioのPublicAssemblyフォルダか、あるいは絶対パスでの指定のいずれかとなるので、

プロジェクト内にあるdllを参照する場合には、以下のようなVisual Studioのマクロを使うのが良いと思われる。

<#@ assembly name="$(SolutionDir)T4SupportClassLibrary\bin\T4SupportClassLibrary.dll" #>

参考:

http://weblogs.asp.net/lhunt/archive/2010/05/04/t4-template-error-assembly-directive-cannot-locate-referenced-assembly-in-visual-studio-2010-project.aspx


ここによると、T4テンプレートのアセンブリディレクティブにdllを明示する方法としては、

  1. Global Assembly Cache(GAC)に入れておく。
  2. dllをフルパスで記述する。
  3. Visual Studioの"C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\PublicAssemblies" のようなVS用のパブリックフォルダに入れておく。
  4. 環境変数を使い、 <#@ assembly name="%mypath%\dotless.Core.dll" #> のように指定する。
  5. Visual Studio Macroを使う。

クラスの生成部

実際のクラス用のコード生成部は、別ファイル「DesigntimeTemplate2.ttinclude」として定義し、それをインクルードしている。

「DesigntimeTemplate2.ttinclude」の中身は、単に変数展開をしているぐらいなので面白みはないが、以下のような感じになる。

    /// <summary>
    /// defClasses.xmlより生成されたクラス <#= classMetaInf.ClassName #>
    /// </summary>
    public partial class <#= classMetaInf.ClassName #>
    {
<#
        for (int idx = 0; idx < classMetaInf.NumberOfColumns; idx++)
        {
#>
        public <#=  classMetaInf.ColumnType(idx) #> <#= classMetaInf.ColumnName(idx) #> { set; get; }
<#
        }
#>

        /// <summary>
        /// 診断用文字列の出力
        /// </summary>
        /// <returns>診断用文字列</returns>
        public override string ToString()
        {
            var buf = new System.Text.StringBuilder();
            buf.Append("<#= classMetaInf.ClassName #>");
            buf.Append("{");
<#
        for (int idx = 0; idx < classMetaInf.NumberOfColumns; idx++)
        {
            if (idx != 0)
            {
#>
            buf.Append(", ");
<#
            }
#>
            buf.Append("<#= classMetaInf.ColumnName(idx) #>=").Append(<#= classMetaInf.ColumnName(idx) #>);
<#
        }
#>
            buf.Append("}");
            return buf.ToString();
        }
    }

ちなみに、「partial class」としているのは生成済みクラスをカスタマイズできるようにするためのものである。

自動生成によってクラスが毎回作り直されるため、生成されたソースファイルに直接メソッドの追加などはできないが、partial classにしておけばカスタマイズ部分は別ファイルにしておくことでコンパイル時に1つのクラスとして結合してくれるわけである。


デバッグおよび強制的な実行

デザイン時テンプレートは、テンプレートを保存するたびに自動的にコードの生成が実行される。

しかし、"defClasses.xml"を変更した場合には感知されないので手動でコード生成を実行する必要がある。

これは簡単で、コンテキストメニューから「カスタムツールの実行」を選択すれば良い。


f:id:seraphy:20140419122836p:image


また、デザイン時テンプレートはVisual Studioが勝手に実行してしまうが、手動でデバッグしたい場合もコンテキストメニューから「T4テンプレートのデバッグ」を選択すれば、ちゃんとブレークポイントも効く状態でデバッグを行うことができる。


生成されたコード例

上記のデザイン時テンプレートを実行すると、以下のコードが自動生成される。

namespace TemplateSample
{
    /// <summary>
    /// defClasses.xmlより生成されたクラス Class1
    /// </summary>
    public partial class Class1
    {
        public System.Int32 val1 { set; get; }
        public System.String val2 { set; get; }

        /// <summary>
        /// 診断用文字列の出力
        /// </summary>
        /// <returns>診断用文字列</returns>
        public override string ToString()
        {
            var buf = new System.Text.StringBuilder();
            buf.Append("Class1");
            buf.Append("{");
            buf.Append("val1=").Append(val1);
            buf.Append(", ");
            buf.Append("val2=").Append(val2);
            buf.Append("}");
            return buf.ToString();
        }
    }

    /// <summary>
    /// defClasses.xmlより生成されたクラス Class2
    /// </summary>
    public partial class Class2
    {
        public System.String val2 { set; get; }
        public System.Int32 val3 { set; get; }
        public System.String val4 { set; get; }

        /// <summary>
        /// 診断用文字列の出力
        /// </summary>
        /// <returns>診断用文字列</returns>
        public override string ToString()
        {
            var buf = new System.Text.StringBuilder();
            buf.Append("Class2");
            buf.Append("{");
            buf.Append("val2=").Append(val2);
            buf.Append(", ");
            buf.Append("val3=").Append(val3);
            buf.Append(", ");
            buf.Append("val4=").Append(val4);
            buf.Append("}");
            return buf.ToString();
        }
    }

    /// <summary>
    /// defClasses.xmlより生成されたクラス Class3
    /// </summary>
    public partial class Class3
    {
        public System.Int32 val3 { set; get; }

        /// <summary>
        /// 診断用文字列の出力
        /// </summary>
        /// <returns>診断用文字列</returns>
        public override string ToString()
        {
            var buf = new System.Text.StringBuilder();
            buf.Append("Class3");
            buf.Append("{");
            buf.Append("val3=").Append(val3);
            buf.Append("}");
            return buf.ToString();
        }
    }

    /// <summary>
    /// defClasses.xmlより生成されたクラス Class4
    /// </summary>
    public partial class Class4
    {
        public System.String val4 { set; get; }
        public System.Int32 val5 { set; get; }

        /// <summary>
        /// 診断用文字列の出力
        /// </summary>
        /// <returns>診断用文字列</returns>
        public override string ToString()
        {
            var buf = new System.Text.StringBuilder();
            buf.Append("Class4");
            buf.Append("{");
            buf.Append("val4=").Append(val4);
            buf.Append(", ");
            buf.Append("val5=").Append(val5);
            buf.Append("}");
            return buf.ToString();
        }
    }

}

このように、単調なコードをズラズラと生成するには、コードジェネレータは抜群に威力を発揮してくれる。


なお、「<# 〜 #>」のブロック記号の前後に何もなく、ただちに改行である場合には、その行は無かったこととみなされる。

そのため、隙間の空いたみっともないコードを生成することはないのも嬉しいところである。


実行時テンプレートの使用

実行時テンプレートはデザイン時テンプレート同様にクラスとして生成されるが、生成されるコードはテンプレートを適用したものではなく、「テンプレートを適用したテキストを生成するためのコード」が生成される点が異なる。


テンプレートの記述方法は基本的に同じであるが、扱い方はずいぶん異なる。


実行時テンプレートの作成

Visual Studio Express 2013 For Desktopで「実行時テンプレート」を新規に作成するには、

「新しい項目の追加」から、「ランタイムテキストテンプレート」を選択する。

f:id:seraphy:20140419122837p:image

このときに指定したファイル名「RuntimeTextTemplate1.tt」をベースとして、ソースコードである「RuntimeTextTemplate1.cs」が自動的に生成されるようになる。


ソリューションエクスプローラから*.ttファイルのプロパティを見ると、カスタムツールが「TextTemplatingFilePreprocessor」となっており、デザイン時テンプレートとは処理方法が異なることが示されている。


初期状態ではテンプレートは空であるが、空であるなりに、何もしないためのテンプレートクラスが自動的に生成される。


生成するテキストテンプレートの定義

今回は、以下のようなHTML的なテキストファイルを実行時に生成するテンプレートを作成してみるものとする。

<#@ template language="C#" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Web" #>
<#@ import namespace="System.Collections.Generic" #>
<!DOCTYPE>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title><#= HttpUtility.HtmlEncode(Title) #></title>
</head>
<body>
<div>
    The date and time now is: <#= DateTime.Now #>
</div>
<div>
    <ul>
<#
    foreach (var item in Items)
    {
#>
        <li><#= HttpUtility.HtmlEncode(item) #></li>
<#
    }
#>
    </ul>
</div>
</body>
</html>
パラメータの定義

実行時テンプレートでは、実行時のプログラムからパラメータをテンプレートに渡す必要がある。

すでに前述のテンプレートでは「Title」や「Items」といった変数を使用している。

これを引き渡すのは簡単であり、以下のようにクラス名を同じにしたPartial Classを作成すれば良いだけである。

    // T4の"実行時テンプレート"は、内部で使用する各種パラメータは
    // 単に、partial classとしてメンバ変数を定義し、それを使用すれば良いだけ.
    // クラス名はテンプレートファイル名から命名される.
    partial class RuntimeTemplate1
    {
        public string Title { set; get; }

        public IEnumerable<string> Items { set; get; }
    }

RuntimeTemplate1.ttは、RuntimeTemplate1というクラスを生成するので、これと同じ名前でPartial Classを作成する。


生成されたRuntimeTemplate1クラスでは、テンプレート中で使用された変数は、そのままソースコードとして出てきているだけなので、Partial Classで変数を定義してあげれば、それでOKとなる。(Partial Classで変数を定義しなければ、変数未定義によりコンパイルエラーになるだけである。)


サポートクラスの定義と、アセンブリの指定

実行時テンプレートはデザイン時テンプレートと異なり、ソースは生成するが、コンパイルはしないし、実行することもない。

ただ単にソースを生成するだけである。


生成されたソースはプロジェクトの他のソースと一緒にコンパイルされることになるため、

テンプレート中では、プロジェクト中の他のクラスを利用するのも制限はなく、アセンブリディレクティブの指定については、まったく必要ない。(プロジェクト側の参照設定に従うため。)


テンプレートの実行

テンプレートクラスを実行するには、生成されたテンプレートクラスに用意されている「TransformText」メソッドを呼び出す。

        /// <summary>
        /// 実行時テンプレートのテスト
        /// </summary>
        private static void runRuntimeTemplate1()
        {
            // 実行時テンプレートをインスタンス化する
            // 実行時テンプレートはカスタムツールは"TextTemplatingFilePreprocessor"として
            // 設定されており、テンプレートの保存時に実行時テンプレートクラスに変換される.
            var tmpl1 = new RuntimeTemplate1();

            // partial classで宣言したメンバ変数に対して値を入れる.
            tmpl1.Title = "Hello, T4 RuntimeTemplate!";
            tmpl1.Items = new List<string>() { "aaa", "bbb", "ccc" };

            // テンプレートを評価する.
            string generatedText = tmpl1.TransformText();

            // 結果の出力
            System.Diagnostics.Debug.WriteLine(generatedText);
            System.Console.WriteLine(generatedText);
        }

非常に簡単に利用できるようになっている。


このメソッドは特に何らかのインターフェイスを実装しているわけでなく、純粋に「TransformText」というメソッドが生成されているだけである。

このテンプレートクラスは、ソースを見れば明らかであるが、内部ではテンプレート処理に必要なさまざまな処理を実装しているが、それ自身で完結しており、T4などの外部のアセンブリは参照していない。

そのため、生成されたテンプレートクラスをアプリケーションに組み込んでも特別なライブラリへの参照設定が増えるようなことはないようになっている。


実行結果

実行結果は以下のようになる。

<!DOCTYPE>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>Hello, T4 RuntimeTemplate!</title>
</head>
<body>
<div>
    The date and time now is: 04/17/2014 21:17:23
</div>
<div>
    <ul>
        <li>aaa</li>
        <li>bbb</li>
        <li>ccc</li>
    </ul>
</div>
</body>
</html>

テンプレートエンジンの実装は巷にたくさんあるし、テキストファイルを読み込んでの変数展開や、ちょっとしたループ処理をするだけなら、自作したとしても、たいして難しい実装が必要なわけでもない。


しかしながら、T4を使えば、C#の全機能を活用でき、且つ、それをソースコードとして取得できるため、実行時にはコンパイル済みとなる高効率なテキスト変換処理を、とても容易く入手することができるわけである。


実行時にガチャガチャと文字列判定処理する非効率さや、サードパーティのライブラリを組み込むような手間を考えれば、T4による実行時テンプレートは非常に強力な選択肢になると思われる。


実行時のT4エンジンの直接利用

これまでのデザイン時テンプレートも実行時テンプレートも、テンプレートそのものは静的なものである。

これに対して、T4のテンプレートエンジンをアプリケーションから直接使うメリットは、

アプリケーションの実行時にテンプレートを動的に変更できる、という点にある。


ただし、この方法での問題は、前述のとおり、T4のテンプレートエンジンにアプリケーションが依存することになるが、このVisual Studio付属の正規のT4のテンプレートエンジンは一般的な再配布ができないコンポーネントである、ということである。

それではVisual Studioをカスタマイズする用途ぐらいにしか使えない。(本来は、それがT4の目的であったわけであるが。)


しかしながら、前述のとおり、MIT/X11でライセンスされている、MonoDevelopの成果物として、T4エンジンとソースコードレベルでほぼ互換性のあるMono.TextTemplatingを用いることで、

ソースコードレベルで、ほぼ互換性のあるT4エンジンを使うことができる。

これであれば一般的なアプリケーションに同梱してもライセンス違反にはならないだろう。


以降は、Mono.TextTemplatingによるT4エンジンの利用について述べるものとする。


Mono.TextTemplatingの入手とビルド

MonoDevelopはGitHubでホストされているので、Cloneするなりしてもらってくる。

https://github.com/mono/monodevelop


"main/src/addins/TextTemplating/Mono.TextTemplating"の下に、T4テンプレートエンジン一式の互換ソースがあり、Mono.TextTemplating.csprojもあるので、Visual Studio Express 2013 for Desktopからでもビルドすることができる。


ただし、ビルドするとだいぶ下の階層のbuild\AddIns\MonoDevelop.TextTemplatingというフォルダにdllが作成されるので、プロジェクト設定を修正して、プロジェクトフォルダの下にdllを生成するようにしておいたほうが良いかもしれない。


ビルドすると、"Mono.TextTemplating.dll"が得られる。


これがT4エンジンの互換アセンブリとなり、アセンブリの名前は"Mono.TextTemplating.dll"だが、中身の名前空間では"Microsoft.VisualStudio.TextTemplating"といったものを定義していたりする。


これをT4テンプレートエンジンを使用するプロジェクトで参照設定すればよい。


ホストの設定と実装

T4エンジンは、ITextTemplatingEngineを実装するクラスであり、

"ProcessTemplate"メソッドを呼び出すことで、

引数に指定したテンプレートをコンパイルして実行し、テンプレートの適用結果を戻り値として返してくれる。


このときテンプレートからアクセスする様々な情報を提供するためのホストオブジェクトを渡す必要がある。

これは、ITextTemplatingEngineHostを実装する必要があり、テンプレート内にパラメータを引き渡す場合には、加えて、ITextTemplatingSessionHostも実装する必要がある。


したがって、まず、ITextTemplatingEngineHostを作成することとする。

大まかな実装例としては、MSDNの以下のページが参考になった。

http://msdn.microsoft.com/ja-jp/library/bb126579.aspx

    /// <summary>
    /// TextTemplatingEngineHostの実装.
    /// 大まかな実装はMSDNの以下のページを参考とした.
    /// http://msdn.microsoft.com/ja-jp/library/bb126579.aspx
    /// </summary>
    [Serializable]
    class TextTemplatingEngineHostImpl
        : ITextTemplatingEngineHost
        , ITextTemplatingSessionHost
    {
        /// <summary>
        /// 現在処理中のテンプレートファイル
        /// </summary>
        public string TemplateFile { set; get; }

        /// <summary>
        /// T4エンジンがコードを生成する先のAppDomain
        /// </summary>
        public AppDomain AppDomain { set; get; }

        /// <summary>
        /// テンプレートに引き渡すデータを保持するセッション
        /// </summary>
        private ITextTemplatingSession session = null;

        public ITextTemplatingSession CreateSession()
        {
            return session;
        }

        public ITextTemplatingSession Session
        {
            get
            {
                return session;
            }
            set
            {
                session = value;
            }
        }

        /// <summary>
        /// オプションを処理する.
        /// </summary>
        /// <param name="optionName"></param>
        /// <returns></returns>
        public object GetHostOption(string optionName)
        {
            object returnObject;
            switch (optionName)
            {
                case "CacheAssemblies":
                    returnObject = true;
                    break;
                default:
                    returnObject = null;
                    break;
            }
            return returnObject;
        }

        public AppDomain ProvideTemplatingAppDomain(string content)
        {
            return AppDomain;
        }

        public string ResolveAssemblyReference(string assemblyReference)
        {
            // フルパスで実在すれば、そのまま返す.
            if (File.Exists(assemblyReference))
            {
                return assemblyReference;
            }

            // "System.Core"のようなアセンブリ参照が来たとき、
            // 標準アセンブリの位置からの相対パスでdllが実在するか判断する.
            foreach (string sysloc in StandardAssemblyReferences)
            {
                string dir = Path.GetDirectoryName(sysloc);
                string candidate = Path.Combine(dir, assemblyReference + ".dll");
                if (File.Exists(candidate))
                {
                    return candidate;
                }
            }

            // このクラスがあるアセンブリの場所からの相対位置で判断する.
            {
                string dir = Path.GetDirectoryName(this.GetType().Assembly.Location);
                string candidate = Path.Combine(dir, assemblyReference + ".dll");
                if (File.Exists(candidate))
                {
                    return candidate;
                }
            }

            // 不明
            return "";
        }

        public Type ResolveDirectiveProcessor(string processorName)
        {
            throw new Exception("Directive Processor not found");
        }

        public string ResolvePath(string fileName)
        {
            if (fileName == null)
            {
                throw new ArgumentNullException("the file name cannot be null");
            }

            if (!File.Exists(fileName))
            {
                // 現在処理中のテンプレートファイルからの相対位置で実在チェックする.
                string dir = Path.GetDirectoryName(this.TemplateFile);
                string candidate = Path.Combine(dir, fileName);
                if (File.Exists(candidate))
                {
                    return candidate;
                }
            }

            // 不明
            return fileName;
        }
        
        public bool LoadIncludeText(string requestFileName, out string content, out string location)
        {
            location = ResolvePath(requestFileName);
            if (File.Exists(location))
            {
                content = File.ReadAllText(location);
                return true;
            }

            content = "";
            return false;
        }

        public void LogErrors(CompilerErrorCollection errors)
        {
            foreach (CompilerError error in errors)
            {
                Console.Error.WriteLine(error.Line + ":" + error.Column +
                    " #" + error.ErrorNumber + " " + error.ErrorText);
            }
        }

        public string ResolveParameterValue(string directiveId, string processorName, string parameterName)
        {
            // テンプレートで、hostSpecific="true" の場合に、<#@ parameter ...#>を使用した場合で、
            // その変数名がSessionにない場合に、このメソッドが呼び出される.
            // (hostSpecific="false"であるか、セッションに変数がある場合は呼び出されない.)
            throw new NotImplementedException();
        }

        private string _extension;

        public string Extension
        {
            get
            {
                return _extension;
            }

            set
            {
                _extension = value;
            }
        }

        public void SetFileExtension(string extension)
        {
            this.Extension = extension;
        }

        private Encoding _encoding = Encoding.UTF8;

        public Encoding Encoding
        {
            get
            {
                return _encoding;
            }

            set
            {
                _encoding = value;
            }
        }

        public void SetOutputEncoding(System.Text.Encoding encoding, bool fromOutputDirective)
        {
            this.Encoding = encoding;
        }

        /// <summary>
        /// 標準で参照するアセンブリの一覧
        /// </summary>
        public IList<string> StandardAssemblyReferences
        {
            get
            {
                var ret = new string[]
                {
                    typeof(System.Uri).Assembly.Location, // System名前空間用
                    typeof(System.Linq.Enumerable).Assembly.Location, // Linq名前空間用
                    typeof(ITextTemplatingEngineHost).Assembly.Location, // T4エンジンのホストインターフェイス用
                };
                return ret;
            }
        }

        /// <summary>
        /// 標準でインポートする名前空間の一覧
        /// </summary>
        public IList<string> StandardImports
        {
            get
            {
                return new string[]
                {
                    "System",
                    "System.Collections.Generic",
                    "System.Linq",
                    "System.Text"
                };
            }
        }
    }
}

Hostオブジェクトは、実行時テンプレート、デザイン時テンプレート共用であるため、他方には意味のないメソッドもある。


重要なのは以下のあたり。

  • ProvideTemplatingAppDomain
    • テンプレートをコンパイルする先のAppDomain.
      • これによりAppDomain.Unloadすれば使用済みのテンプレートクラスを破棄できる.
    • nullを返すと、現在のAppDomain上に作成される.
  • ResolveAssemblyReference
    • アセンブリディレクティブまたはStandardAssemblyReferencesで指定してアセンブリのパスを解決する
  • StandardAssemblyReferences
    • アセンブリディレクティブを使わずともに標準で参照すべきアセンブリの一覧
  • StandardImports
    • 明示的にインポートせずとも標準でインポートされる名前空間の一覧
  • LogErrors
    • テンプレートに誤りがある場合は、これに通知される.

また、T4エンジンが現在のAppDomainとは異なる場所にテンプレートクラスを生成する場合には、ホストオブジェクトがAppDomain間をまたぐことになる。

したがって、このHostクラスと内包するフィールドのすべてはシリアライズ可能でなければならない。


※ MarshalbyRefObjectにすると、RemotingServices.Disconnect()で切断したとしても、なぜかメモリリークしてしまう。このあたり、よくわかっていない。



なお、ProcessTemplateは、テンプレートをコンパイルしテキストを生成後するまでの一連の流れを一発で行うが、生成されたコードは使い捨てであり、一度生成されたコードはAppDomain内にゴミとして蓄積されることになる。

(内部的に生成されたアセンブリはAppDomainにロードされると、AppDomain自体を破棄する以外にアンロードすることはできないため。)

そのため、定期的にAppDomain.Unloadでアンロードする必要がある。


セッションクラスの実装

セッションは単にアプリケーションとテンプレート間のデータの受け渡しのためのコレクションクラスである。

AppDomainをまたぐため、こちらもシリアライズをサポートする必要がある。


※ Dictionaryクラスは、ISerializableによる明示的なシリアライズを実装しているので、派生クラスではGetObjectDataを忘れずにオーバーライドし、

且つ、逆シリアル用コンストラクタの実装も忘れてはならない。


    /// <summary>
    /// セッションの実装.
    /// 単純にDictionaryと、IDを持っているだけのコレクションクラスである.
    /// </summary>
    [Serializable]
    public sealed class TextTemplatingSessionImpl
        : Dictionary<string, Object>
        , ITextTemplatingSession
        , ISerializable
    {
        public TextTemplatingSessionImpl()
        {
            this.Id = Guid.NewGuid();
        }

        private TextTemplatingSessionImpl(
           SerializationInfo info, 
           StreamingContext context)
            : base(info, context)
        {
            this.Id = (Guid)info.GetValue("Id", typeof(Guid));
        }

        [SecurityCritical]
        void ISerializable.GetObjectData(SerializationInfo info,
           StreamingContext context)
        {
            base.GetObjectData(info, context);
            info.AddValue("Id", this.Id);
        }
        
        public Guid Id
        {
            get;
            private set;
        }

        public override int GetHashCode()
        {
            return Id.GetHashCode();
        }

        public override bool Equals(object obj)
        {
            var o = obj as TextTemplatingSession;
            return o != null && o.Equals(this);
        }

        public bool Equals(Guid other)
        {
            return other.Equals(Id);
        }

        public bool Equals(ITextTemplatingSession other)
        {
            return other != null && other.Id == this.Id;
        }
    }

セッションオブジェクトはアプリケーション側からは、以下のように用いる。

    var host = new TextTemplatingEngineHostImpl();
    host.TemplateFile = templateFile;

    ITextTemplatingSession session = new TextTemplatingSessionImpl();
    host.Session = session;
    session["maxCount"] = 20;

これを受けるテンプレート側は以下のようなスクリプトを記述する。

<#@ template language="C#" debug="true" hostSpecific="true" #>
...
<#
// "Host"変数を有効とするために、hostSpecific="true" とすること。
var sessionHost = (Microsoft.VisualStudio.TextTemplating.ITextTemplatingSessionHost) Host;
var mx = (int)sessionHost.Session["maxCount"];
#>

templateディレクティブでhostSpecificがtrueの場合は、Hostオブジェクトをテンプレート内で利用可能となる。

Hostオブジェクトに実装したセッションオブジェクトを取得し、セッションの値を読み取る。



本来、parameterディレクティブが使える場合にはセッションに格納した値は自動的に利用可能になるはずであるが、MonoDevelopのMono.TextTemplatingの実装では、parameterディレクティブを使用するとエラーになってしまう。

(内部的に非公開メソッドをリフレクション経由で使おうとしており、そのあたりで、うまくかみ合っていない。)


もし、T4エンジンをVisual Studio付属(要Visual Studio SDK)のものを参照設定したとすれば、

以下のように書くことができる。

<#@ template language="C#" debug="true" hostSpecific="true" #>
<#@ parameter name="maxCount" type="System.Int32" #>

これでテンプレート内では「maxCount」という変数がセッションの内容としてアクセスできるようになる。


なお、Visual Studio付属のT4テンプレートエンジンを使うために必要なアセンブリとしては、以下のものを参照指定する必要がある。

  • Microsoft.VisualStudio.TextTemplating.12.0
    • Program Files\Microsoft Visual Studio 12.0\VSSDK\VisualStudioIntegration\Common\Assemblies\v4.0\Microsoft.VisualStudio.TextTemplating.12.0.dll
  • Microsoft.VisualStudio.TextTemplating.Interfaces.10.0
    • Program Files\Microsoft Visual Studio 12.0\VSSDK\VisualStudioIntegration\Common\Assemblies\v4.0\Microsoft.VisualStudio.TextTemplating.Interfaces.10.0.dll
  • Microsoft.VisualStudio.TextTemplating.Interfaces.11.0
    • Program Files\Microsoft Visual Studio 12.0\VSSDK\VisualStudioIntegration\Common\Assemblies\v4.0\Microsoft.VisualStudio.TextTemplating.Interfaces.11.0.dll

VSSDKというフォルダが示すように、これは「Visual Studio 2013 SDK」のインストールが必要である。

(これはExpress版にはインストールできない。)


アプリケーション側の呼び出し部

ホストが準備できたら、あとはT4エンジンを呼び出すだけである。

    // テンプレートファイルの読み込み
    var curdir = Path.GetDirectoryName(typeof(Program).Assembly.Location);
    var templateFile = Path.Combine(curdir, "template\\TemplateFile.txt");
    var templateString = File.ReadAllText(templateFile);

    // ホストの設定
    var host = new TextTemplatingEngineHostImpl();
    host.TemplateFile = templateFile;

    // セッションの設定
    ITextTemplatingSession session = new TextTemplatingSessionImpl();
    host.Session = session;
    session["maxCount"] = 20;

    // T4エンジンの構築
    var engine = new Engine();

    // コードを自動生成するドメインを指定する.
    host.AppDomain = AppDomain.CreateDomain("TextTemplate AppDomain");
    try
    {
        // テンプレートクラスの構築と、テキスト生成までの一括処理
        string output = engine.ProcessTemplate(templateString, host);
        Console.WriteLine("output=" + output);
    }
    finally
    {
        // 使い終わったらアンロードする.
        AppDomain.Unload(host.AppDomain);
    }

なお、テンプレートに誤りがある場合は、ProcessTemplateはnullを返すだけである。

エラーはホストのLogErrorsに通知される。


実行例

以下のテンプレートを引き渡すとする。


TemplateFile.txt

<#@ template language="C#" debug="true" hostSpecific="true" #>
<#@ output extension=".txt" encoding="Shift_JIS" #>
<#@ assembly name = "System.Core" #>
<#@ import namespace="System.Linq" #>
<#
var sessionHost = (Microsoft.VisualStudio.TextTemplating.ITextTemplatingSessionHost) Host;
var mx = (int)sessionHost.Session["maxCount"];
#>
<#@ include file="TemplateFile2.txt" #>
done.

TemplateFile2.txt

<#
for (int idx = 0; idx < mx; idx++)
{
#>hello, world! No.<#= idx #>
<#
}
#>

実行結果は以下のようになる。

hello, world! No.0
hello, world! No.1
hello, world! No.2
hello, world! No.3
hello, world! No.4
done.

ホストまわりを準備するのが少々めんどくさいが、使い方そのものは難しくはない。

ただし、実行時に動的にコンパイルし、しかも、一回限りの使い捨てであるため、あまり効率が良いとは言えないかもしれない。


このような単純なテンプレートでも、こちらのテスト環境では一回あたり80mSecほどかかっていた。



実行時のT4エンジンの直接利用 (PreprocessTemplateの利用)

もし、テンプレートを動的にコンパイルしたあと、それを複数回繰り返し利用するのであれば、

ITextTemplatingEngine.PreprocessTemplate

を利用すると良いだろう。


これは、実行時テンプレートに相当するもので、テンプレートを生成するためのコードを文字列として生成する。


引数としてホストを与えなければならないが、ProcessTemplateの場合と異なり、AppDomainなどは返す必要がない。

これは、実際にコンパイルを行うわけではなく、ソースコードを文字列として組み立てるところまでしか実施しないためである。


テンプレートからソースコードへの変換
    // T4 Engine
    var engine = new Engine();

    // テンプレートをソースコードに変換する.(実行時テンプレート)
    string className = "GeneratedClass";
    string namespaceName = "TemplateEngineExample";
    string lang;
    string[] references;
    string generatedSource = engine.PreprocessTemplate(
        templateContent, // テンプレート
        host, // ホスト
        className, // 生成するテンプレートクラス名
        namespaceName, // 生成するテンプレートクラスの名前空間
        out lang, // 生成するソースコードの種別が返される
        out references // 参照しているアセンブリの一覧が返される
        );

上記のように、テンプレートを与えると、そのソースコードと参照しているアセンブリ一覧やコードの種類(C#等)を取得できる。


ソースコードからアセンブリへの変換と、テンプレートクラスのインスタンス

これをCodeDomProviderを使って動的にコンパイルすれば、テンプレートクラスのインスタンスを得ることができる。

    // コンパイラを取得する.
    var codeDomProv = CodeDomProvider.CreateProvider(lang);

    // 参照するアセンブリの定義
    var compilerParameters = new CompilerParameters(references);

    // アセンブリはインメモリで作成する.
    compilerParameters.GenerateInMemory = true;

    // コンパイルする.
    result = codeDomProv.CompileAssemblyFromSource(
        compilerParameters, sourcecode);

    // エラーがあれば例外を返す.
    if (result.Errors.Count > 0)
    {
        var msg = new StringBuilder();
        foreach (CompilerError error in result.Errors)
        {
            msg.Append(error.FileName).Append(": line ").Append(error.Line)
                .Append("(").Append(error.Column).Append(")[")
                .Append(error.ErrorNumber).Append("]")
                .Append(error.ErrorText).AppendLine();
        }
        throw new ApplicationException(msg.ToString());
    }

    // エラーがなければアセンブリを取得し、
    // テンプレートクラスのインスタンスを作成する.
    Assembly assembly = result.CompiledAssembly;

    Type type = assembly.GetType(fqClassName); // 名前空間.クラス名を指定してクラスを取得
    dynamic templateInstance = Activator.CreateInstance(type);

動的にアセンブリを読み込むため、上記コードのコンパイル時点では生成されたクラスの情報は不明である。

そのような場合には、Activator.CreateInstanceで生成したインスタンスはdynamic型で受け取ると便利である。


生成したテンプレートクラスは以下のように利用する。

    templateInstance.Host = host;
    string output = templateInstance.TransformText();

dynamic型で受け取っているので、上記のHostフィールドや、TransformTextメソッドは実行時にリフレクションによって結び付けられる。


基本的には上記コードで、

  1. テンプレートをソースコードに変換する。
  2. ソースコードをアセンブリにする。
  3. アセンブリからテンプレートクラスを構築する。
  4. テンプレートクラスを利用する。

という流れはできる。


ただし、AppDomainを分離していないため、このまま実行すると、テンプレートを構築するたびにクラスが生成されつづけることになる。


AppDomainを分離可能にしたバージョン
    /// <summary>
    /// 実行時テンプレートのインターフェイス
    /// </summary>
    public interface IRuntimeTextTemplate : IDisposable
    {
        /// <summary>
        /// ホスト
        /// </summary>
        ITextTemplatingEngineHost Host { set; get; }

        /// <summary>
        /// テンプレート変換を実施する.
        /// </summary>
        /// <returns></returns>
        string TransformText();
    }

    /// <summary>
    /// テンプレートクラスをAppDomain間で利用できるようするProxy
    /// </summary>
    class RuntimeTextTemplateProxyImpl
        : MarshalByRefObject
        , IRuntimeTextTemplate
    {
        /// <summary>
        /// テンプレートクラスのインスタンス
        /// </summary>
        private dynamic templateInstance;

        /// <summary>
        /// 破棄済みフラグ
        /// </summary>
        private bool _disposed;

        /// <summary>
        /// 初期化。アセンブリをロードする.
        /// </summary>
        /// <param name="assemblyBytes">ロードするアセンブリの内容</param>
        /// <param name="fqClassName">名前空間・クラス名</param>
        public void LoadAssembly(byte[] assemblyBytes, string fqClassName)
        {
            var assembly = Assembly.Load(assemblyBytes);
            templateInstance = (dynamic) assembly.CreateInstance(fqClassName);
        }

        ~RuntimeTextTemplateProxyImpl()
        {
            Dispose(false);
        }

        public void Dispose()
        {
            GC.SuppressFinalize(this);
            Dispose(true);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!_disposed)
            {
                templateInstance = null;

                RemotingServices.Disconnect(this);
                _disposed = true;
            }
        }

        public ITextTemplatingEngineHost Host
        {
            set
            {
                templateInstance.Host = value;
            }

            get
            {
                return templateInstance.Host;
            }
        }

        public string TransformText()
        {
            System.Diagnostics.Debug.WriteLine("current appdomain=" + AppDomain.CurrentDomain.FriendlyName);
            return templateInstance.TransformText();
        }

        public sealed override object InitializeLifetimeService()
        {
            // AppDomainを越えてアクセスするため、マーシャリングされているが
            // 使用期間は不明であるため無期限とする.
            // そのため、使い終わったらDisposeメソッドを呼び出し、Disconnectする必要がある.
            return null;
        }
    }

    /// <summary>
    /// テンプレートクラスを構築するファクトリ
    /// </summary>
    public class RuntimeTextTemplateFactory
    {
        /// <summary>
        /// 生成したテンプレートクラスをロードするAppDomain
        /// </summary>
        public AppDomain TemplateAppDomain { get; set; }

        /// <summary>
        /// T4テンプレートエンジン
        /// </summary>
        private Engine engine;

        public RuntimeTextTemplateFactory()
        {
            this.engine = new Engine();
            this.TemplateAppDomain = AppDomain.CurrentDomain;
        }

        /// <summary>
        /// ファイルを指定してテンプレートを構築する.
        /// </summary>
        /// <param name="templateFile">テンプレートファイル</param>
        /// <returns>テンプレートクラスのインスタンス</returns>
        public IRuntimeTextTemplate Generate(string templateFile)
        {
            string templateContent = File.ReadAllText(templateFile);
            return Generate(templateContent, templateFile);
        }

        /// <summary>
        /// テンプレートとファイルの位置を指定してテンプレートを構築する.
        /// </summary>
        /// <param name="templateContent">テンプレートの内容</param>
        /// <param name="templateFile">テンプレートファイルの位置</param>
        /// <returns>テンプレートクラスのインスタンス</returns>
        public IRuntimeTextTemplate Generate(string templateContent, string templateFile)
        {
            TextTemplatingEngineHostImpl host = new TextTemplatingEngineHostImpl();
            host.TemplateFile = templateFile;

            // 生成するクラス名をランダムに作成する.
            // (アセンブリが毎回異なるので必須ではないが、一応。)
            Guid id = Guid.NewGuid();
            String className = "Generated" +
                BitConverter.ToString(id.ToByteArray()).Replace("-", "");

            // テンプレートをソースコードに変換する.(実行時テンプレート)
            string lang;
            string[] references;
            string generatedSource = engine.PreprocessTemplate(
                templateContent,
                host,
                className,
                "TemplateEngineExample",
                out lang,
                out references
                );
            string fqClassName = "TemplateEngineExample." + className;

            // アセンブリの位置が確定していないものは先に確定しておく
            var resolvedReferences = references.Select(host.ResolveAssemblyReference)
                .Where(x => !string.IsNullOrEmpty(x)).ToArray();


            // コンパイラを取得する.
            var codeDomProv = CodeDomProvider.CreateProvider(lang);

            // 参照するアセンブリの定義
            // アセンブリはテンポラリに作成する.
            var compilerParameters = new CompilerParameters(references);

            // コンパイルする.
            var result = codeDomProv.CompileAssemblyFromSource(
                compilerParameters, generatedSource);

            // エラーがあれば例外を返す.
            if (result.Errors.Count > 0)
            {
                var msg = new StringBuilder();
                foreach (CompilerError error in result.Errors)
                {
                    msg.Append(error.FileName).Append(": line ").Append(error.Line)
                        .Append("(").Append(error.Column).Append(")[")
                        .Append(error.ErrorNumber).Append("]")
                        .Append(error.ErrorText).AppendLine();
                }
                throw new ApplicationException(msg.ToString());
            }

            // エラーがなければ生成されたアセンブリを取得する.
            byte[] assemblyBytes = File.ReadAllBytes(result.PathToAssembly);

            try
            {
                // 生成されたアセンブリファイルは不要になるので削除する.
                File.Delete(result.PathToAssembly);
            }
            catch (Exception ex)
            {
                System.Diagnostics.Debug.WriteLine("Can't delete file: " + ex);
                // 削除失敗しても無視して継続する.
            }

            // ターゲットのAppDomain内でアセンブリをロードするためのプロキシを作成する.
            var proxy = (RuntimeTextTemplateProxyImpl)TemplateAppDomain.CreateInstanceAndUnwrap(
                typeof(RuntimeTextTemplateProxyImpl).Assembly.FullName,
                typeof(RuntimeTextTemplateProxyImpl).FullName);

            // アセンブリをロードさせる.
            proxy.LoadAssembly(assemblyBytes, fqClassName);
            
            return proxy;
        }
    }

AppDomainが絡んでくるようなコードは、いままで書く機会がなかったので、さまざまなサイトやMSDNを読んでみて、何度も試行錯誤して、ようやく動いたコードである。

AppDomain間のマーシャリングが、これで問題ないのか、無駄なことや間違いをしているのではないかと気がかりもあるが、とりあえず、実験したかぎりでは、うまく動いているっぽい。


テンプレートをソースコードに変換し、それをコンパイルした結果のアセンブリを、独自に作成したAppDomain上に読み込ませて、それを利用してテンプレート変換したあと、AppDomainをアンロードする。ということを繰り返してみてもメモリ消費量は一定であるため、おそらくメモリリークはしていないと思われる。


利用例
    var host = new TextTemplatingEngineHostImpl();
    host.Session = new TextTemplatingSessionImpl();

    host.TemplateFile = "TemplateSample1.tt";
    host.Session["maxCount"] = 2;

    // 独自のAppDomainを構築する.
    AppDomain appDomain = AppDomain.CreateDomain("RuntimeTextTemplate AppDomain");
    try
    {
        var fact = new RuntimeTextTemplateFactory();
        fact.TemplateAppDomain = appDomain;

        // テンプレートファイル
        var curdir = Path.GetDirectoryName(typeof(Program).Assembly.Location);
        var templateFile = Path.Combine(curdir, "template\\TemplateFile.txt");

        // テンプレートクラスのAppDomain越えプロキシを作成する
        using (var textTemplate = fact.Generate(templateFile))
        {
            //// 実行する.
            textTemplate.Host = host;
            string output = textTemplate.TransformText();
            Console.WriteLine("output=" + output);
        }
    }
    finally
    {
        // AppDomainをアンロードして動的に生成したアセンブリをメモリから解放する.
        AppDomain.Unload(appDomain);
    }

テンプレートクラスを構築後、

    string output = textTemplate.TransformText();

は何度でも繰り返し実行できる。

何度もテンプレートを利用する場合には、前述のProcessTemplateを使うよりも高速に処理できるはずである。



試したところ、テンプレートの実行そのものは、1回あたり0.1mSec程度だった。

しかし、コンパイルするところまでが非常に遅く、テンプレートクラスが取得できるまで400mSecほどかかっていた。

この数字だと、5回以上テンプレートを使用する用途でないかぎり、ProcessTemplateを単純に使ったほうがよさそうという結果である。

実装が非効率なのか、あるいは、なにか無駄や間違いがあるのかもしれない。

ここは、もう少し詰めてみたほうがよさそうである。

※ 新規のAppDomainでCodeDomProviderでコンパイルすると初回準備に時間がかかっているようである。繰り返し同じAppDomainを使うとコンパイル時間は平均80mSecと、ProcessTemplateと変わらなくなる。なので、改善策としてはプライマリのAppDomain内でコンパイルし、アセンブリはメモリではなく一時ファイルを経由して分離したAppDomain内でロードする、などの方法が考えられる。(4/22追記、コードも訂正済み)


とりあえず、テンプレートからのソースコード生成と、ソースコードからアセンブリの生成、という一連の流れの基礎については、今回の実験で理解できたと思う。


結論

T4テンプレートエンジンの使い方を見てきたが、この実験コードにより、

  • デザイン時テンプレートは、ソースコードの自動生成に使うと大変便利そうである。
  • 実行時テンプレートはテンプレートを使ってテキスト変換処理をするアプリで使うと便利そうである。
  • T4エンジンそのものを直接利用するのはDotNETのフルスペックでテンプレートを書ける強力なツールを作れそうである

、ということが分かった。


また、後半、CodeDomProviderとかAppDomainとか、いままで使う機会のなかった部分について横道にそれてしまった感じがあるが、これらについても、とても良い勉強になった。


こうした動的コンパイルのめんどくささを経験してから、T4の一般的な使い方であるDSL的な用途を考え直してみると、テンプレートからソースコードを生成して静的にコンパイルされる、ということの効率の良さと、取扱いの容易さがよく分かる。


あらためて、T4がよくできたツールだということを感じられた。


以上、メモ終了。

*1:DSLツールキットを入れたMSBUILDの場合は、MSBUILDの環境を示す。

2014-02-14 Java7u51以降でApache Derbyのネットワークサーバを使う場合の設定

[][] Java7u51以降でApache Derbyのネットワークサーバを使う場合の設定  Java7u51以降でApache Derbyのネットワークサーバを使う場合の設定を含むブックマーク

概要

最近アップデートされたJava7u51を使用したところ、

Apache Derby 10.10のネットワークサーバを起動しようとすると、以下のような例外が発生して起動できなくなっていた。

db-derby-10.10.1.1-bin\bin>startNetworkServer
Fri Feb 14 13:15:27 JST 2014 : セキュリティーマネージャーが Basic サーバーセキュ
リティーポリシーを使用してインストールされました。
Fri Feb 14 13:15:27 JST 2014 : access denied ("java.net.SocketPermission" "localhost:1527" "listen,resolve")
...

access denied ("java.net.SocketPermission" "localhost:1527" "listen,resolve")


どうやらセキュリティ例外らしいが、これは何が原因で、どうすれば良いのか調べてみた。


原因と対策

結論的にいうと、これはJava7u51のデフォルトのセキュリティポリシーの変更が原因である。

Java7u51のデフォルトのセキュリティポリシーの変更点

Java7u51によるソケット権限に関する、デフォルトのセキュリティポリシーの変更についてはリリースノートに説明がある。

Java SE Development Kit 7, Update 51 (JDK 7u51)のリリースノートより抜粋

Change in Default Socket Permissions

The default socket permissions assigned to all code including untrusted code have been changed in this release.

Previously, all code was able to bind any socket type to any port number greater than or equal to 1024.

It is still possible to bind sockets to the ephemeral port range on each system.

The exact range of ephemeral ports varies from one operating system to another, but it is typically in the high range (such as from 49152 to 65535).

The new restriction is that binding sockets outside of the ephemeral range now requires an explicit permission in the system security policy.


Most applications using client tcp sockets and a security manager will not see any problem, as these typically bind to ephemeral ports anyway.

Applications using datagram sockets or server tcp sockets (and a security manager) may encounter security exceptions where none were seen before.

If this occurs, users should review whether the port number being requested is expected, and if this is the case, a socket permission grant can be added to the local security policy, to resolve the issue.

これによると、セキュリティマネージャが有効になっている場合のデフォルトのポリシーとして、信頼されていない(=有効な署名がない)アプリケーションでは、エフェメラルポートを除いて、明示的に許可を与えないかぎりポートをバインドすることができない、という制約がつけられるようになったようである。


従来はアプリケーションが信頼されているかを問わず1024以上の、どんなポートもバイント可能だった。


このリリースノートでは、特定のプロトコルでの利用が決められていない一時的な通信のために自由に使うポート(エフェメラルポート)は、個々のOSによるとして具体的に示していないが、一般的には49152から65535の数値の大きなポート範囲で運用することを想定する旨が示されている。


この範囲は、IANAでの提言による"DYNAMIC AND/OR PRIVATE PORTS"(動的・私用ポート)の範囲(49152..65535)に合わせているようである。


OSごとのエフェメラルポートは以下のようになっている。

http://www.wdic.org/w/WDIC/%E3%82%A8%E3%83%95%E3%82%A7%E3%83%A1%E3%83%A9%E3%83%AB%E3%83%9D%E3%83%BC%E3%83%88

  • Linuxではnet.ipv4.ip_local_port_range というカーネルパラメータが示す範囲
    • Kernel2.4以降、CentOSなどRHEL互換だと既定では32768から61000の範囲
    • Kernel2.2時代の古いものは1024から4999の範囲だった。
  • Windows Vista以降ではIANAと同じ (http://support.microsoft.com/kb/929851)
    • XP以前だと既定では1025から5000の範囲だった。
  • BSD系もIANAと同じなので、おそらくMac OS Xも同じではないかと思われる。(OSXはIANAと同じという記述はネット上にたくさんあるが、公式な資料がみつからなかった。)

上記から、最大公約数的には、このIANAで定義されている数値49152以上で、且つ、Linuxの既定の上限である61000までの範囲を使っておけば安全そうではある。


Apache Derbyに付属するネットワークサーバでは既定で1527ポートを使用するために、上記のエフェメラルポートの範囲外となり、

  1. アプリケーションが署名されていない
  2. 明示的にセキュリティポリシーでポートを許可していない

上記の条件に合致するするため、セキュリティ例外が発生することになる。


対策

この対策についても、このリリースノートにちゃんと書いてある。

Area: other-libs/javadb

Synopsis: Additional permission needed to run Java DB network server


An additional permission may be needed in order to bring up the Java DB network server.

In particular, the startup scripts in <db/bin> may fail to boot the network server.

This is a result of the "Better applet networking" changes made by 8011787 (not public).

While attempting to boot, the network server may fail and raise the following error:

access denied ("java.net.SocketPermission" "localhost:1527" "listen,resolve")
java.security.AccessControlException: access denied 
("java.net.SocketPermission" "localhost:1527" "listen,resolve")

To fix this problem, you must bring up the network server with a security policy which includes the missing permission.

Instead of booting the network server as:

java org.apache.derby.drda.NetworkServerControl start

boot the network server as follows:

java -Djava.security.manager -Djava.security.policy=${yourPolicyFile}
org.apache.derby.drda.NetworkServerControl start

where ${yourPolicyFile} is a file containing a customized version of the policy file described in the Java DB Admin Guide section titled Basic Network Server security policy.

You must customize that generic policy file to fit your application.

In addition, you must add the following permission to the permissions block granted to the ${derby.install.url}derbynet.jar codebase:

permission java.net.SocketPermission "localhost:${port}", "listen";

where ${port} should be replaced by the port number where the network server listens for incoming connection requests.

By default, that is port 1527.

For more information on Java DB security policies, see the Java DB Admin Guide sections titled Network Server security and Running the Network Server under the security manager.

If you are using replication, a similar permission must be granted to the security policy for the slave server.

Add the following permission to the ${derby.install.url}derby.jar codebase:

permission java.net.SocketPermission "localhost:${slavePort}", "listen";

where ${slavePort} should be replaced by the port number where the slave server listens for incoming connection requests (typically 4851).

For more information on the security policy for the slave server, see the Java DB Admin Guide section titled Replication and security.

要約すると、ポリシーファイルをつくって1527ポートを許可しろ、ってことである。


ググってみると、この対策の具体的な手順も、先達の方々が、すでに公開されていた。


もし、既存のポリシーを生かしたままSocketPermissionを加えたいのであれば、Derby自身が必要とするポリシーなどについては、公式ドキュメントに記載があるので、これらとあわせて設定すれば良いようである。

http://db.apache.org/derby/docs/10.10/devguide/cdevcbabejdfj.html



...しかし、全権許可するだけにPolicyを設定するぐらいなら、セキュリティマネージャを外したほうがいいんじゃないか、と思う。

パーミッションを変更する必要のない、もっと簡単な方法(開発環境向け)

だれがセキュリティマネージャを設定しているのか?

そもそも、セキュリティポリシーが効いているということは、セキュリティマネージャが明示的に設定されている、ということである。

セキュリティマネージャはJavaの起動時にシステムプロパティで指定するか、プログラム内部から明示的にセキュリティマネージャを設定する必要がある。

ということは、Apache Derbyのネットワークサーバは、いずれかの方法によってセキュリティマネージャを有効にしているはずである。


これを設定しなければセキュリティポリシーは適用されないので、この問題も解決するし、ポリシーファイルを用意するとかめんどくさいことをしなくても良い。


しかし、バッチファイルを流し読んでみたが、セキュリティマネージャを指定するようなシステムプロパティは見つからない。

ということは、プログラム内部から有効にしているはずである。


Apache Derby 10.10はオープンソースなのでソースを見ることができる。


予想通りである。

しかも、セキュリティマネージャを有効にしないための仕組みも用意されていた。


NetworkServerControl.javaの中で以下のようなコードがあった。

    /**
     * Return true if we need to install a Security Manager. All of the
     * following must apply. See DERBY-2196.
     *
     * <ul>
     * <li>The VM was booted with NetworkServerContro.main() as the
     * entry point. This is handled by the fact that this method is only called
     * by main().</li>
     * <li>The VM isn't already running a SecurityManager.</li>
     * <li>The command must be "start".</li>
     * <li>The customer didn't specify the -noSecurityManager flag on the startup command
     * line.</li>
     * </ul>
     */
    private static  boolean needsSecurityManager( NetworkServerControlImpl server, int command )
        throws Exception
    {
        return
            (
             (System.getSecurityManager() == null) &&
             (command == NetworkServerControlImpl.COMMAND_START) &&
             (!server.runningUnsecure())
             );
   }

ソースを読むと、コマンドラインの引数で"-noSecurityManager"と指定されている場合はセキュリティマネージャは設定されないことになっている。


このオプションについてググッてみても公式ドキュメントの記述にはたどり着かなかったのだが、かわりにエラーメッセージに使われていると思われるリソースファイルは見つかった。

ネットワークサーバーの起動に失敗しました。ネットワークサーバーでセキュリティーマネージャーをインストールする前に、ユーザー認証を有効にする必要があります。ユーザー認証を有効にするか、セキュリティーマネージャーのインストールを無効にしてください。 ユーザー認証を有効にする手順については、『Derby Developer''s Guide』の「Working with user authentication」のセクションを参照してください。クライアント/サーバー環境でセキュリティーマネージャーのインストールを無効にすることは、できるかぎり避けてください。どうしても無効にする必要がある場合は、ネットワークサーバーの起動時にコマンド行オプション "-noSecurityManager" を指定して、セキュリティーマネージャーのインストールを無効にすることができます。


興味深いアドバイスが見て取れるが、とりあえず一応、Derbyのネットワークサーバは、セキュリティを外しても動く仕組みはもっているようである。


ネットワークサーバの起動バッチの書き換え

Windowsの場合であれば、startNetworkServer.batを書き換えて、

CALL "%~dp0derby_common.bat" %*

で各種環境変数を設定したあとで、以下の1行を加えれば良い。

set DERBY_CMD_LINE_ARGS=-noSecurityManager %DERBY_CMD_LINE_ARGS%

こうすると、

db-derby-10.10.1.1-bin\bin>startNetworkServer
Fri Feb 14 15:30:26 JST 2014 : Apache Derby Network Server - 10.10.1.1 - (145826
8)が起動し、ポート1527で接続の受入れ準備が完了しました

と、うまく起動できるようになっていることが確認できる。


ちなみに、デフォルトのDerbyのネットワークサーバーのポリシーは、すべてのファイルへの読み書き削除が許可されているので、他のマシンからdbにアクセスすることで、どこにでもDBを作成できてしまったりするので、その点についてはファイアウォールで守らないかぎり(ポリシーがあってもなくても)ザルである。

結論

Derbyが自分の使うポートについてSocketPermissionを設定するのが最適解だと思うし、いずれ、そのように修正されるものと思われるが、

さしあたり、自分のマシンと信頼できる仲間の間の開発用途では「-noSecurityManager」でセキュリティマネージャを外すのも手だと思う。


※ なお、Apache Derbyの埋め込みモードの場合はDerbyは単なるライブラリとなるので、アプリケーション自身のセキュリティ設定に従う。アプリがセキュリティマネージャを設定していなければDerbyもセキュリティを外した状態となるし、仮にセキュリティマネージャを使用していても、埋め込みモードではソケット通信するわけでもないので埋め込みモードのDerbyでは本問題は何の関係もない。

参考文献/URL