C#dynamicの深部へのダイビング

C#4で最も注目すべき追加機能の1つは動的です。 これは何度も何度も説明されています。 しかし、常にDLR(Dynamic Language Runtime)からは見えません。 この記事では、DLRの内部構造、コンパイラー自体の動作を検討し、弱い型と強い型の静的で動的に型付けされた言語の概念を定義します。 そしてもちろん、たとえばGoogle V8エンジンで使用されているPIC手法(Polymorphic Inline Cache)は見過ごされることはありません。



先に進む前に、いくつかの用語と概念を更新したいと思います。



繰り返さないために、式変数とは、任意のデータオブジェクト(変数、定数、式)を意味します。



型チェックの基準によるプログラミング言語は、通常、静的型付け(変数は宣言時に型に関連付けられ、型は後で変更できません)と動的型付け(変数は割り当て時に型に関連付けられ、型は後で変更できません)に分けられます。



C#は静的型付き言語の例ですが、PythonとRubyは動的です。



型安全ポリシーの基準により、弱い言語(変数には厳密に定義された型はありません)と強い/強い言語(変数には厳密に定義された型は後で変更できません)は、入力によって区別されます。



C#4動的キーワード


dynamicは、きれいなコードを記述し、IronPythonやIronRubyなどの動的言語と対話する機能を追加しますが、C#は厳密な型指定の静的型付け言語であることに変わりはありません。



動的メカニズム自体の詳細な説明の前に、コードの例を示します。



//    System.String dynamic d = "stringValue"; Console.WriteLine(d.GetType()); //       d = d + "otherString"; Console.WriteLine(d); //   System.Int32 d = 100; Console.WriteLine(d.GetType()); Console.WriteLine(d); //       d++; Console.WriteLine(d); d = "stringAgain"; //      d++; Console.WriteLine(d);
      
      





実行結果は、以下のスクリーンショットに示されています。







そして、私たちは何を見ますか? ここで何を入力していますか?



すぐにお答えします。タイピングは強力です。その理由は次のとおりです。



C#言語の他の組み込み型(たとえば、string、int、objectなど)とは異なり、dynamicは基本的なBCL型のいずれとも直接比較しません。 代わりに、 dynamicSystem.Objectの特別なエイリアスであり、適切なレイトバインディングに必要な追加のメタデータを備えています。



したがって、次の形式のコード:



 dynamic d = 100; d++;
      
      





以下に変換されます。



 object d = 100; object arg = d; if (Program.<dynamicMethod>o__SiteContainerd.<>p__Sitee == null) { Program.<dynamicMethod>o__SiteContainerd.<>p__Sitee = CallSite<Func<CallSite, object, object>>.Create(Binder.UnaryOperation(CSharpBinderFlags.None, ExpressionType.Increment, typeof(Program), new CSharpArgumentInfo[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) })); } d = Program.<dynamicMethod>o__SiteContainerd.<>p__Sitee.Target(Program.<dynamicMethod>o__SiteContainerd.<>p__Sitee, arg);
      
      





ご覧のとおり、 オブジェクト型の変数d 宣言されています。 次に、Microsoft.CSharpライブラリのバインダーが登場します。



DLR


コード内の各動的式に対して、コンパイラーは、操作自体を表す個別の動的呼び出しノードを生成します。



だから、ビューコードのために



 dynamic d = 100; d++;
      
      





このフォームのクラスが生成されます:



 private static class <dynamicMethod>o__SiteContainerd { // Fields public static CallSite<Func<CallSite, object, object>> <>p__Sitee; }
      
      





<> p__Siteeフィールドのタイプは、System.Runtime.CompilerServices.CallSiteクラスです。 もっと詳しく考えてみましょう。



 public sealed class CallSite<T> : CallSite where T : class { public T Target; public T Update { get; } public static CallSite<T> Create(CallSiteBinder binder); }
      
      





Targetフィールドは汎用ですが、常にデリゲートです。 そして、上記の例の最後の行は、操作の単なるバリエーションではありません:



 d = Program.<dynamicMethod>o__SiteContainerd.<>p__Sitee.Target(Program.<dynamicMethod>o__SiteContainerd.<>p__Sitee, arg);
      
      





CallSiteクラスの静的Createメソッドは次のとおりです。



 public static CallSite<T> Create(CallSiteBinder binder) { if (!typeof(T).IsSubclassOf(typeof(MulticastDelegate))) { throw Error.TypeMustBeDerivedFromSystemDelegate(); } return new CallSite<T>(binder); }
      
      





[ ターゲット]フィールドはL0キャッシュです(L1キャッシュとL2キャッシュもあります)。これは、呼び出し履歴に基づいて呼び出しをすばやくディスパッチするために使用されます。



呼び出しノードが「自己学習」しているため、DLRは定期的にTarget値を更新する必要があることに注意してください。



DLR操作のロジックを説明するために、このテーマに関するEric Lippertの回答を示します(無料翻訳)。



まず、ランタイムは、処理するオブジェクトのタイプ(COM、POCO)を決定します。



次に、コンパイラーが作用します。 レクサーとパーサーは必要ないため、DLRは特別なバージョンのC#コンパイラーを使用します。これは、メタデータアナライザー、セマンティック式アナライザー、およびILの代わりに式ツリーを生成するコードジェネレーターのみを備えています。



メタデータアナライザーは、リフレクションを使用してオブジェクトのタイプを確立し、セマンティックアナライザーに渡して、メソッドの呼び出しまたは操作の実行が可能かどうかを判断します。 次に、ラムダ式を使用しているかのように、式ツリーが構築されます。



C#コンパイラは、キャッシュポリシーとともに式ツリーをDLRに返します。 次に、DLRはこのデリゲートを呼び出しノードに関連付けられたキャッシュに保存します。



これを行うには、CallSiteクラスのUpdateプロパティを使用します 。 [ターゲット]フィールドに格納されている動的な操作を呼び出すと、バインダーが呼び出されるUpdateプロパティへのリダイレクトが発生します。 次回の呼び出し時に、上記のアクションを繰り返す代わりに、既製のデリゲートが使用されます。



多態性インラインキャッシュ


動的言語のパフォーマンスは、呼び出しのすべての場所で実行される追加のチェックと検索のために低下します。 簡単な実装では、クラスの優先度リストでメンバーを常に検索し、コードの行が実行されるたびにメソッド引数の型をオーバーロードできるようにします。 静的型付け(またはコードと型推論に十分な数の型指示がある)を使用する言語では、すべてのダイヤルピアに適した実行時関数への命令または呼び出しを生成できます。 静的型を使用すると、コンパイル時に必要なものすべてが通知されるため、これが可能になります。



実際には、同じタイプのオブジェクトに対して繰り返される操作は、共通のタイプに減らすことができます。 たとえば、整数xおよびyの式x + yの値の最初の計算中に、2つの整数を加算するコード断片または実行時間の正確な関数を思い出すことができます。 その後、後続のすべての計算で、この式の値は、キャッシュのおかげで関数またはコードを検索するために必要なくなります。



上記のデリゲートキャッシングメカニズム(この場合)は、呼び出しノードが自己学習および更新である場合、多態性インラインキャッシュと呼ばれます。 なんで?



ポリモーフィック 。 呼び出しノードのターゲットは、動的操作で使用されるオブジェクトのタイプに基づいて、いくつかの形式をとることができます。



インライン CallSiteクラスのインスタンスのライフサイクルは、呼び出し自体の場所で正確に行われます。



キャッシュ 作業は、さまざまなキャッシュレベル(L0、L1、L3)に基づいています。




All Articles