Lambda 式に invokedynamic を使うのかもしれない話

さきほどの記事で Lambda 式を「構文糖」と書きましたが、実はそうじゃないのかもしれません。 Java 7 で追加された invokedynamic 命令を使って、 Lambda 式の実行を効率化する、という話があるからです。

以下、 Brian Goetz の From Lambdas to Bytecode という資料から理解できた範囲を書きます。 2011 年に開催された JVM Language Summit で発表されたものみたいです。現時点での仕様のドラフトは JSR 335 の添付文書 metafactory.pdf をご覧ください。また、 invokedynamic については 2月の JJUG Night Seminar で使った Java 7 invokedynamic の概要をお薦めします。

例として、次のようなコードを考えてみます。

// リストの要素を倍にして戻す
Iterable<Integer> doubleList() {
    return numbers.map(n -> n * 2);
}

現時点のビルドでは、 Lambda 式は匿名クラスのインスタンス生成と同じバイトコードを生成します。これには性能面の欠点があります。たとえば、コンパイル時に Lambda 式ごとにクラスが作られるので、 Lambda 式をたくさん使うとクラスロードが大変になります。たとえば、上述のコードは次のようにコンパイルされます。

// 現状は、匿名クラスのインスタンス生成と同じ
Iterable<Integer> doubleList() {
    return numbers.map(new $AnonymousMapper$());
}

// Lambda 式ごとにこんなクラスが作られる
class $AnonymousMapper$ implements Mapper<Integer, Integer> {
    Integer map(Integer n) {
        return n * 2;
    }
}

対案としては、 Lambda 式の中身を static メソッドにして、この static メソッドを呼び出す MethodHandle を作り、それを元のインタフェースでラップする、という方法が考えられます。この方式の問題は、 MethodHandle の呼び出しが、まだチューニングされていないので、相当遅いということです。

// MethodHandle を使う
Iterable<Integer> doubleList() {
    MethodHandle mh = MethodHandles.lookup()
        .findStatic(getClass(), "$anonymousMapper$", methodType(Integer.class, Integer.class));
    Mapper mapper = MethodHandles.asInstance(mh, Mapper.class);
    return numbers.map(mapper);
}

static Integer $anonymousMapper$(Integer n) {
    return n * 2;
}

ということで、しばらくは匿名クラス方式がいいけど、いずれは MethodHandle 方式に移行したい。でも、匿名クラスのインスタンスを作る処理をバイトコードに直接書いてしまうと、どこかの時点で Lambda 式を使ったコードを再コンパイルしてもらう必要がある。これはかっこ悪いです。

そこで、 invokedynamic を使えば、どちらの方式で行くのかを、コンパイル時ではなく実行時に決められるじゃん!というのが Goetz の言い分です。このやり方では、 Lambda 式は invokedynamic 命令の呼び出しに変換され、初回実行時に呼び出されるブートストラップメソッドが、どちらの方式で行くのかを決めます。

// 選択をブートストラップメソッドに任せる。最早 Java ソースでは書けない
Iterable<Integer> doubleList() {
    Mapper mapper = <invokedynamic LambdaMetaFactory#metaFactory, "$anonymousMapper$">
    return numbers.map(mapper);
}

// 匿名クラス経由、あるいは MethodHandle 経由で呼ばれる
static Integer $anonymousMapper$(Integer n) {
    return n * 2;
}

上のコードで、 LambdaMetaFactory#metaFactory は JVM 実装が提供します。このブートストラップメソッドは、しばらくの間は匿名クラスを実行時に生成して、そのインスタンスを作る戦略を選ぶでしょうし、いずれ MethodHandle がカリカリチューンされたら、 MethodHandle を生成する戦略を選ぶようにするでしょう。その際、 Lambda 式を使っている既存のソースを再コンパイルする必要はありません。

さあどうだ。素敵じゃない?

所感

なんだかずいぶんトリッキーだけど、面白い。 JVM 実装の自由度を広げるために invokedynamic を使うわけですね。今後 Java 9 とか Java 10 とかで新しい言語要素が追加される時に、命令を追加したり、特定の命令列を吐き出したりする代わりに、とりあえず invokedynamic で逃げる、とかできるわけだ。