The Dynamic Language Runtime and the Iron Languages 日本語訳(後半)

この文書は、The Architecture of Open Source Applications Volume II: Structure, Scale, and a Few More Fearless Hacksに収録されているThe Dynamic Language Runtime and the Iron Languagesの日本語訳です。原文と同様、日本語訳もcc-by unported 3.0によって公開されます。

これは日本語訳の後半部分。前半はこちらにあります。

8.7. 動的コールサイト

静的な.NET言語では、どのコードが呼び出されるかは、全てコンパイル時に決定されていました。例えば、次のようなC#のコードがあったとします:

var z = x + y;

C# コンパイラは、'x' および 'y' の型と、それらが加算可能であるかどうかを知っています。C#コンパイラは、演算子オーバーローや型変換、その他このコードを適切に処理するために必要と なる、適切なコードを出力できます。これは、関連する型についての、完全に静的な情報に基づいています。ここで、以下のPythonコードを考えてみま しょう:

z = x + y

IronPythonコンパイラは、これが出現した時に、何をする必要があるのか、全くわかりません。 xy が何であるかを知らず、仮に知ったとしても、 xy の加算の可否は、実行時には変わっているかもしれないからです。(原理的には可能かもしれませんが、IronPythonIronRubyも、型推論を 行いません。) IronPythonは、数値を加算するILコードを生成する代わりに、これを実行時に解決するためのコールサイトを出力します。

コールサイトは、処理(operation)を実行時に決定するためのプレースホルダーです。これらは System.Runtime.CompilerServices.CallSiteインスタンスとして実装されています。RubyPythonのような動的言語では、全ての処理が動的コンポーネントです。動的な処理は DynamicExpression ノードの式ツリーに表現されます。式ツリーコンパイラは、これをコールサイトに変換すべきであると知っています。コールサイトが生成された時点では、期待された処理をどのように行うかは、未知の状態です。しかし、これは現在使用中の言語に固有のコールサイト・バインダーインスタンスを用いて生成され、その処理を行うために必要な情報を全て含んでいます。

Figure 8.1: コールサイトのクラス図

図8.1: コールサイトのクラス図

 そ れぞれの言語には、それぞれの処理に応じた別々のコールサイト・バインダーが存在し、それらのバインダーは、そのコールサイトに渡される引数に応じて処理 を実行する方法を、時として数多く、知っています。しかしながら、それらのルールを生成するのはコストが高くつくため(特に、実行できるデリゲートへの変 換して.NETのJITの呼び出しを伴うなど)、このコールサイトには、複数レベルのコールサイト・キャッシュがあり、ここには後々の利用のために生成されたルールを格納します。

コールサイトのフローチャート

最 初のレベルL0は、コールサイトのインスタンス自身をあらわす CallSite.Target プロパティです。ここには、このコールサイトで最も直近に使用したルールが保存されます。大半のコールサイトについては、ひと組の引数型について呼び出さ れるのみなので、これだけが必要です。このコールサイトには別のキャッシュL1があり、ここには他に10件のルールを格納できます。もし Target がこの呼び出しに対して有効でない場合(たとえば、引数型が異なった場合)、このコールサイトはまずそのルールキャッシュをチェックして、以前の呼び出し で既に適当なデリゲートが生成されていないかを見て、それを新しいものを生成する代わりに再利用します。

ルールをキャッシュに格納するとい うのは時間的な都合によるものです。新しいルールを実際にコンパイルするのは、既存のルールをチェックするよりも時間がかかります。大まかに言えば、ルー ルの述語として最も一般的である、ひとつの変数に対する型チェックを行うのには10ナノ秒かかります(二値関数のチェックには20ナノ秒、以下同様)。一 方、doubleを加算する単純なメソッドをコンパイルするには、だいたい80マイクロ秒かかります。数千倍長いのです。このキャッシュのサイズは、全て のコールサイトで使用される全てのルールを記憶してメモリを浪費しなうよう、限られています。単純な加算については、それぞれのバリエーションで約1KB のメモリが必要になります。しかし、プロファイリングが示す結果では、10種類のバリエーションを要するコールサイトはほとんどありません。

最 後に、バインダーのインスタンス自身に格納されるL2キャッシュがあります。ひとつのコールサイトに関連付けられるバインダーのインスタンスには、その コールサイト固有の追加情報を格納するかもしれませんが、いずれにしろ、コールサイトの大半はユニークなものではなく、同一のバインダーインスタンスを共 有できます。たとえば、Pythonで、加算の基本的なルールはプログラム全体を通して同一です。これは + の両端にある2つの型に依存する、それだけです。そのプログラム中では、全ての加算処理は同一のバインダーを共有し、もしL0とL1のキャッシュが無い場 合、このL2キャッシュには、プログラム全体を通して収集された、ずっと多く(128件)の最新ルールが含まれています。もしあるコールサイトが初めて実 行されるとしても、このL2キャッシュに適当なルールが見つかる可能性が十分にあります。これが最も効率的に機能するよう、IronPythonIronRubyのいずれも、加算など共通の処理で使用される、典型的な(canonical)バインダーインスタンスの集合をもっています。

L2キャッシュが無かった場合、 このバインダーは、そのコールサイトに、現在の引数型を(あるいはさらに値も)考慮した実装を作成するよう要求します。上記の例では、もし xy がdoubleであれば(あるいは他のネイティブ型であれば)、この実装は単純にそれらをdoubleにキャストして、ILの add 命令を呼び出します。このバインダーは、引数をチェックしてそれらがこの実装に適合することを保証するためのテストも生成します。この実装とテストが組み 合わさって、ひとつのルールになります。ほとんどの場合、実装とテストは式ツリーとして生成され格納されます。(コールサイトのインフラストラクチャー は、式ツリーに依存しません。デリゲート単体で利用されることもあります。)

もしこの式ツリーがC#で表現されたとしたら、 それは次のようなものになります:

if(x is double && y is double) {       // double型をチェック
      return (double)x + (double)y;    // doubleなら実行
 }
 return site.Update(site, x, y);       // doubleでないので、その型に
// 応じたルールを探索/作成

バインダーは、その後、この式ツリーからデリゲートを 生成して、ルールをILに、さらにマシンコードに、コンパイルします。2つの数値を加算する場合、これは、型のクイックチェックと数値を加算するマシン命 令になるでしょう。以上の全てを含めても、最終的な結果は静的なコードよりごくわずかに遅いだけでしょう。IronPythonIronRubyには、 プリミティブ型の加算など共通の処理のコンパイル済みルールが含まれており、実行時の生成を不要にして時間を節約して、代わりにディスクスペースを幾分か 余計に消費しています。

8.8. メタオブジェクトプロトコル

言語インフラストラクチャーとは別の、DLRのもうひとつの主要な部分は、ある言語(ホスト言語)が、別の言語(ソース言語) で定義されたオブジェクトに、動的な呼び出しを行える能力です。これを可能にするために、DLRは、あるオブジェクト上でどの処理が有効であるかを、その オブジェクトを作成した言語を問わずに、判断できなければなりません。PythonRubyは類似のオブジェクトモデルを有していますが、 JavaScriptは根本的に異なる、プロトタイプベースの(暮らすベースとは異なる)型システムを有しています。DLRでは、これらの様々な型システ ムを統合する代わりに、これらをSmalltalkスタイルのメッセージ渡しに基づいて扱います。

メッ セージ私のオブジェクト指向システムでは、オブジェクトは他のオブジェクトに(普通はパラメータ付きで)メッセージを渡し、そのオブジェクトは結果として またオブジェクトを返します。こうして、オブジェクトの何たるかをそれぞれの言語において構想できるようにしつつ、メソッド呼び出しをオブジェクト間の メッセージとして見るだけで、それらのほぼすべてを同等に扱えるようになるのです。もちろん、静的なOO言語においてすらも、このモデルはある程度適合し ます。動的言語で違うのは、呼び出されるメソッドがコンパイル時に既知である必要はない、あるいはオブジェクト上に存在していなくても良い(たとえば Rubymethod_missing )、あるいは、ターゲットオブジェクトが通常的に必要に応じてメッセージに介入して、異なるやり方で処理する機会があります(たとえばPython__getattr__ )。

DLRでは、以下のメッセージが定義されています:

  • {Get|Set|Delete}Member : オブジェクトのメンバーを操作する処理
  • {Get|Set|Delete}Index : インデックスのあるオブジェクトの処理(配列や辞書など)
  • Invoke , InvokeMember : オブジェクトのメンバーの呼び出し
  • CreateInstance : オブジェクトのインスタンスの生成
  • Convert : オブジェクトをある型から別の型に変換する
  • UnaryOperation , BinaryOperation : 否定( ! )や加算( ! )のような、演算子ベースの処理を実行する

これらがあれば、どんな言語のオブジェクトモデルを実装するにも十分であるはずです。

CLRは本来的に静的型付けなので、動的言語のオブジェクトもやはり静的なクラスによって表現されなければなりません。通常のテクニックは、 PythonObject のような静的なクラスを作り、実際のPythonオブジェクトがそのクラスあるいはその派生クラスのオブジェクトとなるようにする、というやり方です。相 互運用性とパフォーマンスという理由から、DLRのメカニズムは、それよりはるかに複雑なものです。言語固有のオブジェクトを扱う代わりに、DLRでは、 System.Dynamic.DynamicMetaObject のサブクラスで、上記のメッセージを扱うメソッド群を含む、メタオブジェクトを扱います。それぞれの言語には、その言語のオブジェクトモデルを実装した DynamicMetaObject のサブクラスがあります。IronPythonでは MetaPythonObject です。これらのメタクラスは、対応する System.Dynamic.IDynamicMetaObjectProtocol インターフェースを実装するクラスをもちます。これが、DLRで動的オブジェクトを識別する方法として用いられます。

Figure 8.3: IDynamicMetaObjectProtocolのクラス図

DLRは、 IDynamicMetaObjectProtocol を実装するクラスから、GetMetaObject() を呼び出して DynamicMetaObject を取得できます。この DynamicMetaObject は言語によって提供され、そのオブジェクトに必要なバインディングの機能を実装します。それぞれの DynamicMetaObject には、もし提供されていれば、そのオブジェクトの内部的な値と型も含まれます。最後に、DynamicMetaObject は、コールサイト バインダーにも似ていますが、現時点でのコールサイトをあらわす式ツリーと、その式に対するあらゆる制約を格納しています。

DLRは、ユーザー定義クラス上のメソッドに対する呼び出しをコンパイルする時、まずはコールサイト(すなわち CallSite クラスのインスタンス)を生成します。このコールサイトは、上記の "Dynamic Call Sites" で説明される通りのバインディング プロセスを開始し、これは最終的に OldInstanceインスタンス上にあって MetaOldInstance を返す GetMetaObject() を呼び出します。(Pythonは古いスタイルと新しいスタイルのクラスがありますが、この話はそれとは無関係です。) 次に、バインダーが呼び出され( PythonGetMemberBinder.Bind() )、続いて MetaOldInstance.BindGetMember() が呼び出されます。これは、そのオブジェクトからメソッドを名前でルックアップする方法を記述した、新しい DynamicMetaObject を返します。すると、もうひとつのバインダー PythonInvokeBinder.Bind() が呼び出され、これは MetaOldInstance.BindInvoke() を呼び出し、先ほどの DynamicMetaObject に、ルックアップされたメソッドを呼び出す方法を、ラップします。つまり、ここには、元のオブジェクト、メソッド名をルックアップする式ツリー、そのメソッドへの引数を表す DynamicMetaObject 、が含まれます。

いったん式の中で最終的な DynamicMetaObject が生成されたら、この式ツリーおよび制約が、そのバインディングを開始したコールサイトへ返されます。そうすると、このコードは、そのコールサイト キャッシュの中に格納することができ、他の動的呼び出しと同等の速度で、そして静的呼び出しとほぼ同等の速度で、そのオブジェクト上の操作を行うことができるのです。

動的言語上の動的な処理を実行したいホスト言語は、そのバインダーを DynamicMetaObjectBinder から派生しなければなりません。この DynamicMetaObjectBinder は、まずターゲット オブジェクトに対して処理のバインドを要求して(これは前述の GetMetaObject() の呼び出しとそれ以降のバインディングのプロセスによります)、それからホスト言語のバインディングのセマンティクスにフォールバックします。つまり、たとえば IronRubyのオブジェクトがIronPythonのプログラムからアクセスされる場合、このバインディングはまずRuby(ターゲット言語)のセマンティクスを試みられ、もしそれが失敗したら、 この DynamicMetaObjectBinder はPython(ホスト言語)のセマンティクスにフォールバックしてきます。もしバインドされるオブジェクトが動的でなかった場合(つまり IDynamicMetaObjectProvider を実装していない場合)、たとえば.NETの基本クラスライブラリのクラスであった場合、これは.NETのリフレクションを用いて、ホスト言語のセマンティクスでアクセスされます。

言語側でこれをどのように実装するかについては、多少の自由があります。IronPythonPythonInvokeBinderInvokeBinder から派生していません。Pythonオブジェクトに固有の追加処理が必要になるためです。Pythonオブジェクトのみを扱っている限り、これは問題ありません。もし IDynamicMetaObjectProvider を実装しているが Python のものではないオブジェクトに遭遇した場合、これは InvokeBinder を継承していて外部のオブジェクトも正常に処理できる CompatibilityInvokeBinder クラスに、処理を委譲します。

フォールバックによって処理をバインド出来なかった場合、例外は投げられません。代わりに、エラーを表す DynamicMetaObject が返されます。そうしたら、ホスト言語のバイダーは、そのホスト言語なりの適切な手法に基づいて、これを扱います。たとえば、仮定的なJavaScript実装において、IronPythonのオブジェクト上に対して、存在しないメンバーにアクセスしようとした場合は undefined が返されることになり、同様にIronPythonからJavaScriptのオブジェクトに対して同様の操作を行うと AttributeError が発生することになるでしょう。

8.9. ホスティング

DLRは、言語共通の実装の詳細に加えて、共有されたホスティング インターフェースも提供しています。このホスティング インターフェースは、ホスト言語によって(通常はC#のような静的言語です)、PythonRubyといった別の言語で書かれたコードを実行するために使用されます。これは、ユーザーがアプリケーションを拡張できるようにするための一般的なテクニックであり、DLRはこれをさらに進めて、DLR実装を有する任意のスクリプト言語を簡単に使用できるようにしました。このホスティング インターフェースには、4つの主要な部品があります。ランタイムエンジン>ソース、>そしてスコープです。

ScriptRuntimeは、一般的には、ひとつのアプリケーション中のほぼ全ての動的言語で共有されます。このランタイムは、ロードされている言語で使用されている全ての現在利用可能なアセンブリ参照を扱い、ファイルのクイック実行を行うメソッドを提供し、新しいエンジンを作成するメソッドを提供します。単純なスクリプティングのタスクには、このランタイムのみが必要なインターフェースとなりますが、DLRではスクリプトの実行方法をより柔軟に制御できるようにするクラスも提供します。

通常、ひとつのスクリプト言語にはひとつの ScriptingEngine が使用されます。DLRのメタオブジェクトプロトコルは、ひとつのプログラムが複数の言語からのスクリプトをロードでき、それぞれの言語で作成されたオブジェクトがシームレスに相互運用できる、ということを意味します。このエンジンは、言語固有の LanguageContext (たとえば PythonContextRubyContext )をラップして、ファイルや文字列からコードを実行して、DLRをネイティブでサポートしない言語(たとえば.NET 4以前のC#)から、動的オブジェクトの処理を行うために使用されます。エンジンはスレッドセーフであり、各スレッドが独立したスコープにある限り、複数のスクリプトを同時に実行できます。スクリプトソースを作成するメソッドも提供されており、これによってスクリプトの実行をより細かく制御できるようになっています。

ScriptSource は、実行されるコード片を表します。これは、実際のコードを保持する SourceUnit オブジェクトを、そのソースを作成した ScriptEngine にバインドします。このクラスは、コードをコンパイルするか(そうすると CompiledCode オブジェクトが出力され、キャッシュ可能になります)、直接実行するかを選べるようにします。もしこのコード片が繰り返し実行されるようであれば、まずコンパイルして、そのコンパイル済みコードをスクリプト上で実行するのがベストです。一度しか実行されないコードは、直接実行するのがベストです。

しかし、最終的にコードが実行される際には、ScriptScope が実行されるコードについて提供されなければなりません。このスコープは、スクリプトの全変数を保持し、もし必要であれば、変数と合わせてホストから事前ロードできるようになっています。これによって、スクリプトの実行時にホスト側からカスタムオブジェクトが提供できるようになります。たとえば、画像エディタは、そのスクリプトで処理する画像のピクセルデータにアクセスする方法を提供するかもしれません。いったんスクリプトが実行されると、生成されたいかなる変数も、このスコープから読み取れます。スコープのもうひとつの主な用途は、独立化の実現です。これによって、複数のスクリプトが相互に干渉し合うこと無く同時にロードして実行できるようになります。

これらのクラス全てが、言語ではなくDLRから提供されているということが重要です。エンジンによって使用されている LanguageContext のみが、その言語実装に由来しています。この言語コンテキストが、コードのロード、スコープの生成、コンパイル、実行、動的オブジェクトの処理といった、ホスト言語によって必要とされる全ての機能を提供し、残りのDLRホスティングクラスが、それらの機能にアクセスするための便利機能を提供します。これによって、同じホスティングのコードが、任意のDLRベースの言語をホストできるというわけです。

Cで書かれた動的言語実装(オリジナルのPythonRuby)については、動的言語で書かれていないコードにアクセスするためには、特別なラッパーコードが書かれなければならず、これはサポートするスクリプトごとに行われる必要があります。これを簡単にするSWIGのようなソフトwらは存在しますが、それでもPythonRubyのスクリプティング インターフェースをプログラムに組み込んで、そのオブジェクトモデルを外部スクリプトによる実行に向けて公開するのは、簡単なことではありません。しかし、.NETのプログラムについては、ランタイムをセットアップして、プログラムのアセンブリをランタイムにロードして、 ScriptScope.SetVariable() を使用してプログラムのオブジェクトをスクリプトから利用可能にする、というだけのことで、スクリプティングが簡単にできます。.NETアプリケーションでスクリプティングのサポートを追加するのは、ほんの僅かな時間で可能であり、DLRの大きなボーナスポイントであります。

8.10. アセンブリ レイアウト

DLRは、CLRの一部とは独立して発展してきたため、CLRに含まれる部分(コールサイト、式ツリー、バインダー、コード生成、動的メタオブジェクト)とIronLanguagesオープンソースプロジェクトに含まれる部分(ホスティング、インタープリター、そしてここで議論しなかったいくつかの部分)が分かれています。CLRに含まれている部分は、IronLanguagesプロジェクトにも Microsoft.Scripting.Core として含まれています。DLRの部分はさらに2つのアセンブリに別れます。Microsoft.ScriptホスティングAPIを、 Microsoft.Dynamic はCOM相互運用、インタープリター、その他動的言語の共通部品を、それぞれ含んでいます。

言語自体も、同様に2つに分かれていて、 IronPython.dllIronRuby.dll は言語自体(パーサー、バインダーなど)を実装していて、 IronPython.Modules.dllIronRuby.Libraries.dll は、クラシックPythonおよびRubyの実装において、Cで実装されている標準ライブラリの部分を実装しています。

8.11. ここから得られた教訓

DLRは、静的ランタイム上で構築された動的言語のための言語中立プラットフォームとして有用な例です。ここで高パフォーマンスな動的コードを実現するために用いられたテクニックは、適切に実装するのはトリッキーなものなので、DLRがそのテクニックを引き受けて全ての動的言語の実装で利用できるようにしたのです。

IronPythonおよびIronRubyは、DLR上で言語をビルドする良い例です。実装は近いチームが同時に開発していたために非常に類似していますが、実装にはやはりそれなりの違いが見られます。共同で開発された複数の異なる言語(IronPythonIronRubyJavaScriptのプロトタイプ、完全に動的なバージョンのVBと言われる謎のVBx)と、C#およびVBのdynamicの機能によって、DLRの設計は開発中に多大なテストを得られることになりました。

IronPythonIronRuby、そしてDLRの、実際の開発は、同時期にMicrosoftで行われていたプロジェクトの大半とは、大きく異なるかたちで行われてきました。非常にアジャイルな反復的開発モデルであり、初日から継続的インテグレーションが稼動していました。これによって、必要に応じて非常に素早く物事を変更することが出来て、DLRをC#のdynamic機能として開発の早い時点で統合できたこともあって、良いことでした。DLRのテストは非常に速く、十数秒程度で終わるものですが、言語のテストを実行するには長い時間がかかります(IronPythonのテストスイートは、並列実行してもだいたい45分かかります)。この部分を改善することで、反復のスピードを改善することができたでしょう。最終的には、これらの反復は現在のDLRの設計に収束され、部分的には非常に複雑になるでしょうが、全体的には両者がきわめて良いかたちに適合することでしょう。

DLRがC#に統合されたことは、DLRの居場所が確保され「目的」をもったという点で、大変に重要なことでしたが、いったんC#のdynamic機能が完了すると、政治的な雰囲気も変わり(たまたま景気動向が変わったこともあり)、Iron言語は社内で急速に支持を失って行きました。たとえば、ホスティングAPI.NET Frameworkに統合されることはありませんでした(そしてほぼ間違いなく、今後も無いでしょう)。これはつまり、PowerShell 3は、これもまたDLRに基づいているのですが、IronPythonIronRubyとは全く異なるホスティングAPIの集合を使用し、しかし前述の通りそのオブジェクトは相互運用できる、ということになります。(DLRチームのメンバーの何人かは、IronPythonIronRubyホスティングAPIの魅力的な代替品を生み出す、C#のサービスとしてのコンパイラ、コードネーム "Roslyn"を実現するライブラリの仕事に回りました。) しかし、オープンソースライセンスの驚異の力によって、これらは生き残り続け、さらに繁栄し続けることでしょう。