Archive for January, 2008
Notes on an Architecture for Dynamic Invocation(Translation)
JSR 292: Supporting Dynamically Typed Languages on the JavaTM Platformの仕様作成に向けた覚え書きが、チェアをつとめているJohn Rose氏のブログに掲載されていました。勉強をかねて勝手に和訳してみました。
ーーーーーーーーーーーー ここから ーーーーーーーーーーーー
Introduction
以前のブログでJVMにおけるcallサイトにかかわる様々な要素を列挙しました。他の言語、とくにダイナミックな言語のプログラム構造をサポートするために、JVMは現在のメソッド定義、実行、リンケージ手法を拡張しなければなりません。高度に成長し、成熟したJVMの実装インフラストラクチャを再利用したいので、まったく新らしいメカニズムを作りたくはありません。
私が代表となっているJSR292エキスパート・グループはバイトコード・アーキテクチャへの修正を注意深く選び出しながら、この種の問題を詳細にあげる仕様作りをおこなっています。最初のEDR(early draft review)は数週間のうちに参照できるようになります(訳注:2007年12月13日から数週間)。そこで、多言語の世界でメソッド呼出しに必要となる設計をいくつかあげておこうと思います。鍵となる新しい機能はダイナミックな再リンク、プログラム可能なリンケージ、プログラム可能なガード、識別子多態性、メソッド・ハンドラです。
簡単に説明すると:
- * ダイナミックな再リンクとはダイナミックなリンケージが無効化されるかどうかを決められるようにすること
- * プログラミング可能なリンケージとはダイナミックなリンケージが”ブートストラップ・メソッド”をリフレクション可能にするかどうかを決められるようにすること
- * プログラム可能なガードとはcallサイトが言語特有のルールにしたがってオーバロードされた複数のメソッドから選べるようになっていること
- * 識別子多態性とはcallサイトとアダプタが普通に管理され、関連付けられるようにすること
- * メソッド・ハンドルとはブートストラップ・コードとcallサイトに直接バイトコード・メソッドを参照させること
- * シンボリックな型指定子が代替可能な複数のメソッドから選び出す手助けとなります(Lispのように)
- * ストラクチャ・テンプレート マッチングがタグつけされたツリーのような構造の場合に使われます(Haskell参照)
- * バックトラッキングありの単一化マッチングがPrologでは適用可能性を決めています。
- * 等価性チェックがダイナミックなスイッチに適しています(PyPyで行っているように)
- * レシーバの型は明示的にインタフェースとメソッドを実装している
- * メソッド名はインタフェースで決められる
- * 識別子(型のシグニチャ)もインタフェースで決められる
- * ほとんど制約を定義されていない値の処理する(ボックス化されたintegerやコレクションのようなプラットフォームの型)
- * 様々な名前で、また、プログラム内で作られた名前でさえもメソッドを呼出す
- * 呼出された側(callee)がどれなのかを満足に知らないような状況で呼出し側(caller)が選んだ識別子にしたがってメソッドを実行する
- * レシーバが所望のメソッドを実装する
- * メソッドがinvokeinterface命令で指定される名前と識別子を持つ
- * 通常のレシーバは制約を受けない
- * ターゲット・メソッドはメソッド名で実行する方法を実装する。
- * ターゲット・メソッドはインストラクションの識別子にしたがって通常のレシーバと他の引数をを受け入れる。おそらくいくぶんかのバリエーションがあるはずです。
- * ボクシングとアンボクシング
- * サブタイプの一つへの参照の変換
- * サプタイプへの参照のキャスト
- * 孤立したメソッドから作る
- * データの値と断片的なコードから作る
- * いくつかの引数に対して実行する
- * アダプタを作る
- * 引数と戻り値の型を尋ねる
- * ダイナミックな再リンクは言語がそのアプリケーション構造を動的に変更できるようにします
- * プログラム可能なリンケージは言語へのメソッド・リンケージへの完全な権限を与えます
- * プログラム可能なガードはポスト・リンケージ、各呼出しごとの型チェックを表現するのに必要です
- * 識別子多態性は呼出しがシミュレーションのオーバヘッドがなくネイティブなJVMのスピードで実行できるようにします
- * メソッド・ハンドルは言語にネイティブなJVMのスピードでバイトコードになったメソッドの実行できるようにします
Dynamic Relinking(ダイナミックな再リンク)
ほとんどの言語やシステム同様、Javaでも各callサイトがダイナミックにリンクされると便利です。つまり、呼出しという意味は最初に実行されるまで完全には決定されていないということです。この意味では、様々なシンボルが参照され互いにマッチングされるのかもしれません。その結果、JVMはメソッド呼出しがどこに向けて行われているのか、レシーバの型が該当するメソッドを選び出すのにどのように影響するかを決めることになります。
多くの言語にとって、callサイトという意味は常に、部分的に暫定的となっていて、今後発生するイベントがメソッド呼出しによる影響を変更できるということです。Javaはクラスやメソッドを再定義できませんが、他の言語はできるようになっているので、callサイトは場合によっては”再リンク”され、以前に呼び出されたメソッドがもはやターゲットにならないようになっています。大抵のJVMはクラスローディングに対応するためこれにこれまで単相性のメソッドだったものが多態性になれる場合に、似た何かをすでに行っています。(HotSpotには”非最適化(deoptimization)”と呼ばれるオペレーションがあり、同様の効果を発揮します。)ここでの新規機能はcallサイトが言語独自のランタイムの制御下において、なんらかの方法で修正されるようになることです。
この機能を満足する簡単な方法はcallサイトを消去できるようにすることで、これによって再リンクを強制できるようにすることです。しかし、(メソッドのローディングやアンローディングのような)プログラム構造における変化を考慮しなければなりません。もっとも簡単な方法はcallサイトが特権オペレーションのたぐいで、どこにあっても消去できるようにすることでしょう。複雑な方法をとるとしたら、callサイトが詳細にわたりリフレクションされ、修正されることで、ダイナミックなランタイムが常に、JVMの肩越しに見ていられるようにすることでしょう。
Programmable Linkage(プログラム可能なリンケージ)
JVMは決められたルールにしたがってダイナミックなリンケージを行います。これらのルールはJava言語を念頭に置いて設計されています。たとえば、callサイトがリクエストしたシンボリックなメソッドはリンケージ後に識別され、解決されたメソッドかたまたま実行された実際にあるメソッドに正確に一致していなければなりません。メソッド名と識別子はすべてのメソッドの中で完全に一致していなければなりません。これらのメソッドの唯一のクラス(あるいはインタフェース)だけが型の階層関係にしたがって型変換されるようになっています。callサイトにオーバロードされる可能性のあるメソッドのセットは解決されたクラス(あるいはインタフェース)のサブタイプで定義されている実在のメソッドに限られます。
しかしながら、とくにダイナミックな言語ではそれぞれのメソッド呼出し側がさまざまな方法で同一のメソッドを呼び出すのはよくあることです。つまり、(多言語JVMでは)全てのcallサイトが実際に定義されているメソッドの呼出しているわけではないということです。識別子やメソッド名のミスマッチが起こりえます。さらに、たまたま呼び出された実在のメソッドの場合、呼出しのシンボリックな型、あるいはシンボリックな識別子内で参照されているクラス(あるいはインタフェース)に関連する型の階層構造をもっているかどうかわかりません。
メソッド呼出しが明らかにレシーバの引数のクラスに向けられている場合は、メソッド呼出しが最初に一般的なアダプタメソッドに送られるのはいかにもありうることです。絶対的なターゲット・メソッドを呼び出す前に(あるいは呼び出した後に)、アダプタ・メソッドは言語特有の記録や引数の変換のようなことを実行します。あるいは、メソッド呼出しはいずれバイトコードに委ねられるそのメソッドをエミュレートする言語独自のインタープリタに飛んでいくこともあるでしょう。
これらを考慮すると、ダイナミック・リンケージはプログラミング可能でなければなりません。JVMがJavaのcallサイトにリンクするハードコードされたアルゴリズムを動かすのと並行して、JVMは言語独自のブートストラップ・メソッドが言語特有のcallサイトにどのようにリンクするかを決められるように処理を譲れるようになっていなければなりません。ブートストラップ・メソッドはcallサイトと実際の引数の情報があたえられて、どのようにメソッド呼出しを送り出すかを決めることになるでしょう。
その後に、今後、起こりうる似たようなメソッド呼出しをどのように扱うかという指示がJVMに戻ってこなければなりません。この時点で、callサイトがリンクされることになります。言っておきたいのは、以前よりもさらにリンクされるという点です。これは、プログラムが実行されていく過程で、基本的に同一のcallサイトで後に新しいタイプが現れる可能性が大きいためです。
Programmable Guards(プログラミング可能なガード)
プログラミング可能なガードは適用可能性を決定しなければならない次なる問題です。ブートストラップ・メソッドがcallサイトの実在するメソッドをリンクした後で、そのメソッドがその時点で新しい実在する引数のセットに適用できるか、あるいはリンケージのプロセスを再び始めなければならないかを調べる必要があるでしょう。
(Javaではcallサイトがリンクされた後ではメソッドが予想していなかった引数を適用できない場合に再リンクするチャンスはありません。)
加えて、callサイトがオーバーロードされた場合(ほとんどのダイナミックな言語ではできなければなりません)、callサイトには違う引数の組合せを適用可能な複数のメソッドが実在するかもしれません。
ここで必須となる概念は適用可能性です。適用可能性というのはメソッド呼出しごとにチェックされる状態のことで、最低でも、ひとつ前にリンクされたメソッドに対しては通常、trueであると考えられるものです。
したがって、ガードという概念が必要になってきます。ここでガードとはcallサイトの引数を分類し、実在のメソッドをを呼び出すべきかどうかを決定するちょっとしたロジックを意味します。あるいは、どれとどのメソッドが呼び出されるかです。Javaのcallサイトでは、レシーバ・オブジェクトのクラスを調べ、クラス内で定義されているメソッドにもっとも一致するものを選び出すことで実現されています。
次にあげるのはダイナミックに引数を分類するのに使われる手法です:
言語はレシーバ越しに引数を見ているかもしれません。これはマルチプル・ディスパッチと呼ばれています。この方法は算術演算のような幾何学的な対称性があるオペレーションや、引数の制約に関するいくつもの一時的な最適化を行うオペレーションには有用です。
ガードはガードするべきメソッドに分離できない方法でリンクされています。実際、ガードはメソッドにくっついている”ミニメソッド”のたぐいと考えるよりも、メソッドの序文であると考えたほうがいいのです。メソッドの序文であるガードは入力される引数を嗅ぎ回り、なにかまずいことがあれば、callサイトに別のメソッドを選ばせたり、再リンクしたり、エラーを投げるよう強制することでメソッド実行をささえます。
ガードに失敗した場合に備えて、メソッド呼出しを”バック・アウト”するための特別な一連のコードがなければなりません。この仕組のために新しいバイトコードを発明するというよりは、JVMがガードに失敗したことを知らせられるようにシステム定義の例外の型を使えるようにするといいでしょう。これはコストがかかる方法ではありません(参照)。
HotSpot JVMはいくつかのメソッド呼出しを高速化するためにこれに似たテクニックを使っています。ソースコードを見てみると、”verified”と”unverified”なメソッドのエントリ・ポイントに違いがあることがわかるでしょう。前者はメソッドの適用可能性を決めるためにメソッドをガードする短い前置きのようなコードです。ガードに失敗した場合、システムはエラーを投げるか、呼出し命令を修正して遅いけれどよりパワフルな(vtabelに基づいた)一連のメソッドを呼出します。ダイナミックな言語には類似のメカニズムがふさわしいでしょう。
目的のメソッドに対するアダプタをスタックしていけるかどうかの可能性はガードの設計をおもしろくしてくれます。ガード・アダプタは受け渡される引数、ガードに失敗したときに呼出し側に制御を戻すこと、正常な制御がうまくいくように絶対的なターゲットに引き渡すことをチェックするようになるでしょう。ターゲットは順番に違うガードを適用するかもしれませんし、なにか別の関数のアダプタになるかもしれません。
Descriptor Polymorphism(識別子多態性)
これまで説明してきたメカニズムはJavaのコードで簡単にシミュレーションできます。callサイトは関連付けられたステートフルなオブジェクトに対するメソッド実行で表現可能でしょう。言語特有のロジックなら”このcallサイトをリンクする”ようなオペレーションを定義している抽象クラスの具象サブクラスで表現できるでしょう。メソッドはどんなふうに見えるでしょうか?java.util.concurrent.Callableインタフェースがヒントを提供してくれています:callサイトから呼び出された実際のメソッドは単一の呼出し(あるいは実行)メソッドを持つインタフェース・オブジェクトのような何かでなければなりません。
The problem with interfaces
そのようなインタフェースに関連する問題としては狭義に制約され、その制約を満足するいくつかのメソッドにしかアクセスできるだけになってしまうということです:
対照的に、ダイナミックな言語ではこれらの制約を破棄しなければならないことがよくあります:
別の見方をすると、それぞれの呼出しの明確な形にあったインタフェースを定義するのは実用的ではありません。さらに、別の方法でhが、すべての呼出しを強制的にひとつのインタフェースで扱うようにするのも実用的ではありません。関数の型をJavaに置き換えるプロポーザルでは中間地点での解決を模索しています。しかし、言語を実装している人からみれば、問題となるのはインタフェースが正しい汎用化をせずに、型の情報を持ちすぎている点でしょう。
What about reflection?
別の方面からみてみましょう。java.lang.reflect.Methodは想定できるメソッドをすべて実行する方法を提供していますが、二つの深刻な問題があります。一つ目ですが、アダプタは一部がデータによって制御され、絶対的なターゲット・メソッドを間接的に呼び出す多目的のメソッドとして(通常は)実装されるので、リフレクティブなメソッドとなる汎用的なアダプタを表現する方法はないということです。しかし、リフレクティブなメソッドはデータ・コンポーネントを持っていません;
リフレクティブなメソッドに関する二つめの問題はすべてに対応できるような一つの識別子(型のシグニチャ)を作るという点で、シミュレーションのオーバヘッドが大きくなります。プリミティブな型はボックスされなければいけませんし、値、発生した例外、引数のリストも返さなければなりません。リフレクティブなメソッドは呼出しシーケンスを汎用化するのと引き換えにJVMのネイティブなメソッド実行のシーケンスを放棄することになります。
Polymorphism without simulation
以下にあげているのはあまりうれしくない代替案ですが、これらはまだ利用されていないJVMの中間地点に位置するもので、シミュレーションのオーバヘッドも無く、ネイティブに実行する呼出しシーケンスのための本当の多態性に関するものです。
仮定的な命令(それ自信)はこの覚え書きにある他の要求機能と一致していません。特に、リンケージに関するところには問題があります。リンケージのところで取り上げたプログラミング可能な呼出しをリンクするためには呼出しを行うターゲット・メソッドが分岐できるようになっていなければなりませんし、ターゲット・メソッドの権利という点でメソッドはオブジェクトでならなければなりません。(そうでなければアダプタはを作れません。)本当のダイナミックなメソッド実行のシナリオにはメソッド呼出し(おそらく最初の引数で見分けられる)を行う通常のレシーバと、メソッドのガードを満足する(可能なら)ターゲット・メソッドの二つのレシーバが存在します。先に触れたように、ターゲット・メソッドというのはアダプタかもしれませんし、実際にバイトコードになっているメソッドへの直接的な参照かもしれませんし、ツリー構造の中を渡り歩くインタプリタのための何か複雑なクロージャのようなものかもしれません。
メソッド実行命令の中のメソッド名はリンケージのプロセスで重要な意味がありますが、ターゲット・メソッドを実際に実行するには各々のメソッド名に対応するアダプタのアーキテクチャを再構築しなくていいように、(invokeのような)システム・ワイドの名前を使うべきです。したがって、呼出しに関する制約を調整すると:
このようなメソッド呼出しにおいては識別子は中心的な役割をはたしてはいないので、呼出し側(caller)と呼び出される側(callee)の識別子が正確に一致するというい要件が緩められるのは当然です。そこで、呼出し側(caller)が(おそらく)意図しているものとベリファイアがチェックしたシステムの型統合の両方を維持しなければなりません。しかし、ベリファイアがフレームの型状態の中にスタックされた型と呼出し識別子の型の間をシフトできるようにするだけで、JVMは以下のような安全なシフトを許せるようになります:
最後の一つは失敗するかもしれないのでやや疑わしいところです。しかし、型消去(type erasure)が簡単にしてくれるので、多くの言語にとって便利な機能でブートストラップ・メソッドがアダプタを作る作業を簡単にしてくれるでしょう。ターゲット・メソッドがJavaの型消去のルールと”うまく動作する”のであれば、必要な機能です。
どのようにブートストラップ・メソッドがターゲット・メソッドをダイナミックなcallサイトのなかにリンクするのか、メソッド・ハンドルに関係付けるのかを考える前に、もうちょっとみておくべきところがあります。
Method Handles(メソッド・ハンドル)
ブートストラップ・メソッドはダイナミックなメソッド実行サイト用のターゲット・メソッドを提供する必要があります。次に何が起きるでしょうか?ダイナミックな言語がJavaのような呼出し(おそらくStringやOutputStreamのようなシステムレベルのオブジェクトに向けられた)をする場合は、必要になるのはコンパイル時と実行時の双方で所望のメソッドを探して見つけることをエミュレートするだけです。ブートストラップ・メソッドはcallサイト内のメソッドにリンクするはずです。
このリンケージはリフレクティブなメソッドを伴って行われるでしょう。しかし、これはとても制限された解決方法です。新しい参照やできる限り直接的に、素早く所望のバイトコードになっているメソッドを参照するメソッド・ハンドルを定義するほうがいいでしょう。どのようなインタフェースを実装するべきでしょうか?答えは:一つだけで十分です。というのは、インタフェースはシグニチャを越えられるほど汎用的ではないからです。バイトコードになっているメソッドに正確に一致する識別子を伴って、メソッド・ハンドル オブジェクトはあたかも名前指定で(あるいは全ての標準的な名前で)実行されるメソッドを持っているかのように振る舞わなければなりません。ブートストラップ・メソッドはこのメソッド・ハンドルを返せます。このメソッド・ハンドルは(これまで説明してきたように)所望の方法で呼出しが完了できるようにするでしょう。
JVMはどこにもインスタンスに特化されたメソッドを持っていないので、これはとても奇妙です。しかし、もしも任意のメソッドに適用するなら、識別子多態性はそのような奇妙なオブジェクトを必要としています。実際には、識別子自信はオブジェクトのインタフェースの変わりに働いています。効果的ではありませんが、確かに、JVMは隠れたところでそのようなインタフェースとともにメソッド・ハンドルを実装することになるでしょう。JVMがバイトコードになっているメソッドに直接分岐することで、迅速なオブジェクト依存の識別子チェックを行うようなメソッド呼出しを実装することになるでしょう。
The interesting operations on method handles are the same as with any system of functional types:メソッド・ハンドルの面白いオペレーションはどの関数型のシステムにもあるものと同じです:
JVMの型構造に関する改革を行わなくてもJVMのバイトコードの中でどのように一つ一つのオペレーションが表現できるかを解説するのはいずれ、詳細な設計の問題になるでしょう。できれば、標準のJava APIで表現できるようになっているべいきです。もちろん、インナー・クラスやクロージャがアダプタやインタプリタのエントリ・ポイントとして使えるように、invokeメソッドのレシーバの手がかりを十分に提供できるはずです。また、カリー化(currying)、可変引数(varargs)管理、あるいはこれらに似た変換を実行する高次の関数を提供できるでしょう。このような関数はボクシングによって引数のリストをシミュレートできるリフレクティブなスタイルのAPIを経由して制御できるでしょう。しかし、JVMはボクシング無しで、不透明な(opaque)メソッド・ハンドル自信を実装できるでしょう。
通常のJavaのメソッド呼出しがリンケージを行うときに正当なアクセスであるかをチェックするように、ダイナミックなメソッド呼出しもチェックされなければなりません。詳細なところがうまくいく場合は、メソッド・ハンドルがバイトコードとして作られたときにアクセスチェックは(ブートストラップ・メソッドによって、呼出し側(caller)に代わって)行われたことがわかります。一度、ハンドルが生成されると、これ以上のアクセスチェックは必要なくなります。(すべてのメソッド呼出しに対するアクセスのチェックはリフレクティブなメソッド同様大きなオーバヘッドとなります。)プライベートなメソッドにアクセスする特権はそのメソッドに対するハンドルを受け取るもの全てに与えられます。この方法は現在のシステムよりもセキュアではなくなるでしょうか?これは違います。というのは、ハンドルを生成したモジュールはそもそも、そのメソッドへのアクセス権を持っていなければならないし、同じメソッドの回りに別のハンドルのたぐいをラップしてそれを自由に渡せるようになっていたからです。メソッド・ハンドルはリフレクティブなメソッドよりも安全で、もともと速いのです。
Conclusion
ここにある設計のロジックがダイナミックな言語が必要としている機能であることを十分に説明できているといいと思っています。これらはちょっとした週末のハックではなく、JVMを拡張することでJVMが今日注目されてきている新しい言語においても役に立つものとなるはずです。
簡単にまとめると:
You are currently browsing the Servlet Garden blog archives for January, 2008.