Reflection.Emitを使用してMSILで式をプリコンパイルする

こんにちは、Habr! スティーブマーシュによる記事「 Reflection.Emitを使用して式をMSILにプリコンパイルする 」の翻訳を紹介します。



はじめに



このプロジェクトのクラスを使用すると、ユーザーが入力したテキスト式を解析し、それらを.NETアセンブリにコンパイルできます。 このアセンブリは、オンザフライで実行するか、DLLに保存できます。 式の事前コンパイルにより、高度な移植性が実現し、ユーザーが入力したロジックを非常に効率的に評価できます。 さらに、Microsoftのildasm.exeツールを使用して、生成されたMSIL基本コードを開いて検証できます。 .NETプラットフォームには多くの興味深い機能がありますが、Reflection.Emit名前空間には、見つけることができる以上の機能があります。 Reflection.Emit名前空間を使用すると、.NETタイプを動的に作成し、MSIL命令を本文に挿入することにより、実行時に独自の.NETコードを作成できます。 MSILは、Microsoftの.NETプラットフォーム用のミドルウェア言語です。 ILは、.NETプログラムの実行時にC#およびVB.NETコードがコンパイルされ、JITコンパイラに送信されます。 MSILは非常に高速で非常に低レベルの言語であり、MSILを使用すると、プログラムを非常に優れた方法で制御できます。 この記事ではMSILの詳細については触れませんが、インターネット上には他にもいくつかのリソースがあります。詳細については、この記事の最後にいくつかのリンクを記載しています。



参考情報



パーサー/コンパイラーが行うことを簡単に見てみましょう。 ユーザーは、パーサーの文法に一致する文字列式を入力します。 この式は、実行される小さな.NETプログラムに変換され、

結果を出力するために、アナライザーは、以下に示すように、文字の連続リストを読み取り、階層ツリーに分割します。 ノードはこの順序で評価されます。 ノードがマップされると、そのタイプのノードに対応するコマンドが呼び出されます。 たとえば、数字が一致すると、その数字をスタックにプッシュします。 トークン「*」が一貫している場合、乗算命令などを呼び出します。 すべての指示を正しい順序で追加すると、右側に示す「プログラム」が表示されます。



画像



次に、プログラムの実行方法を確認し、元のテキスト式と比較します。 最初の2つのコマンドは、整数3と2をスタックにプッシュします。 マルチプライコマンドは、これら2つの値をスタックからプルし、それらを乗算して、6の結果をスタックに送り返します。 命令番号4は整数1をスタックに送信します。 命令番号5は2つの値(6と1)をプッシュし、それらを加算して結果(7)をスタックに返します。 最後に、returnコマンドは値7をスタックからポップし、結果として返します。 これはほとんどのプログラマーにとってシンプルで明白に思えるかもしれませんが、この賢いアイデアはプログラミングとコンパイルの基本であり、一見の価値があると思います。 MSILでこのプログラムがどのように見えるかを次に示します。 たとえば、ldc.r8はロード定数コマンドで、double 3.0をスタックにロードします。



IL_0000: ldc.r8 3. IL_0009: ldc.r8 2. IL_0012: mul IL_0013: ldc.r8 1. IL_001c: add IL_0023: ret
      
      





コードの使用



このプロジェクトには、式を解析してMSILにコンパイルするための2つのクラスが含まれています。 最初のクラスはRuleParserです。これは、特定の文法のすべての字句解析ロジックを含む抽象解析クラスです。 このクラスはメッセージを解析しますが、アクションは実行しません。 上記のコードスニペットは、ttAddトークンが見つかると、パーサーがmatchAdd()メソッドを呼び出すことを示しています。matchAdd()メソッドは、RuleParserクラスで定義された抽象メソッドです。 クラスメソッドと対応するセマンティックアクションの実装は、特定のクラスに依存します。 このテンプレートを使用すると、セマンティックアクションを処理するための個別の具象クラスを実装できます。つまり、実行しようとしている内容に応じて、さまざまな具象クラスを実装できます。 このコードは、検出されたノードをすぐに計算することにより、その場で式を評価するように設定されていました。 これで、MsilParserを交換して、同じパーサークラスを使用して式をILプログラムにコンパイルできるようになりました。 たとえば、matchAdd()関数は単にAddコマンドを挿入します。 変数がマップされると、Ldstrコマンドを使用して変数名をロードし、GetVarメソッドを呼び出します。



 protected override void matchAdd() { this.il.Emit(OpCodes.Add); } protected override void matchVar() { string s = tokenValue.ToString(); il.Emit(OpCodes.Ldstr, s); il.Emit(OpCodes.Call, typeof(MsilParser).GetMethod( "GetVar", new Type[] { typeof(string) })); }
      
      





すべてのトークンを設定したら、MsilParserクラスのCompileMsil()メソッドを呼び出すことができます。このメソッドは、パーサーを起動し、Relection.Emit名前空間のAssemblyBuilderクラスを使用して、コンパイル済みの.NETタイプを返します。



 /// <summary> /// Builds and returns a dynamic assembly /// </summary> public Type CompileMsil(string expr) { // Build the dynamic assembly string assemblyName = "Expression"; string modName = "expression.dll"; string typeName = "Expression"; string methodName = "RunExpression"; AssemblyName name = new AssemblyName(assemblyName); AppDomain domain = System.Threading.Thread.GetDomain(); AssemblyBuilder builder = domain.DefineDynamicAssembly( name, AssemblyBuilderAccess.RunAndSave); ModuleBuilder module = builder.DefineDynamicModule (modName, true); TypeBuilder typeBuilder = module.DefineType(typeName, TypeAttributes.Public | TypeAttributes.Class); MethodBuilder methodBuilder = typeBuilder.DefineMethod(methodName, MethodAttributes.HideBySig | MethodAttributes.Static | MethodAttributes.Public, typeof(Object), new Type[] { }); // Create the ILGenerator to insert code into our method body ILGenerator ilGenerator = methodBuilder.GetILGenerator(); this.il = ilGenerator; // Parse the expression. This will insert MSIL instructions this.Run(expr); // Finish the method by boxing the result as Double this.il.Emit(OpCodes.Conv_R8); this.il.Emit(OpCodes.Box, typeof(Double)); this.il.Emit(OpCodes.Ret); // Create and save the Assembly and return the type Type myClass = typeBuilder.CreateType(); builder.Save(modName); return myClass; }
      
      





最終結果は、実行、キャッシュ、またはディスクへの保存が可能な.NETアセンブリです。 コンパイラーによって作成されたメソッドのILコードを以下に示します。



 .method public hidebysig static object RunExpression() cil managed { // Code size 36 (0x24) .maxstack 2 IL_0000: ldc.r8 3. IL_0009: ldc.r8 2. IL_0012: mul IL_0013: ldc.r8 1. IL_001c: add IL_001d: conv.r8 IL_001e: box [mscorlib]System.Double IL_0023: ret } // end of method Expression::RunExpression
      
      





このアプローチの主な利点は、式の解析に単に命令を実行するよりもはるかに時間がかかることです。 ILで式をコンパイルする前に、式を評価するたびにではなく、一度だけ解析する必要があります。 この例では1つの式しか使用していませんが、実際の実装にはプリコンパイルされ、オンデマンドで実行される数千の式が含まれる場合があります。 さらに、コードは優れた.NET DLLにパッケージ化されており、必要なことは何でもできます。 この例は、100分の3秒の100万倍以上の速度で推定できます!



サンプルプロジェクトの使用



サンプルプロジェクトでは、左上のテキストボックスに式を入力できます。 [分析]をクリックすると、フォームは式を解析し、RunExpression()関数でコンパイルされたコードを使用して.NETアセンブリを作成します。 その後、プログラムはこの関数を特定の回数呼び出し、実行にかかった時間を表示します。 最後に、プログラムはアセンブリをexpression.dllとして保存し、Microsoftのildasm.exeファイルを実行してアセンブリの完全なMSILコードを表示し、プログラム用に生成されたコードを表示できるようにします。



関心のある問題



動的メソッドがどのように呼び出されるかは、パフォーマンスに大きく影響します。 たとえば、動的メソッドで単にInvoke()メソッドを使用すると、100万回呼び出されるとパフォーマンスが大幅に低下します。 以下のコードのように汎用デリゲートサブスクリプションを使用すると、パフォーマンスが約20倍向上します。



画像



 // Parse the expression and build our dynamic method MsilParser em = new MsilParser(); Type t = em.CompileMsil(textBox1.Text); // Get a typed delegate reference to our method. This is very // important for efficient calls! MethodInfo m = t.GetMethod("RunExpression"); Delegate d = Delegate.CreateDelegate(typeof(MsilParser.ExpressionInvoker<>), m); MsilParser.ExpressionInvoker<> method = (MsilParser.ExpressionInvoker<>)d; // Call the function Object result = method();
      
      





*空の山かっこ内はObjectである必要があります。



ILDASM.EXEを呼び出す



サンプルプロジェクトでは、新しく作成したアセンブリのすべてのMSILコードを表示することもできます。 これを行うには、バックグラウンドでildasm.exeを呼び出し、結果をテキストボックスに表示します。 Ildasm.exeは、ILコードまたはSystem.Reflection.Emit名前空間を扱う人にとって非常に便利なツールです。 以下のコードは、System.Diagnostics名前空間を使用してプログラムでこの実行可能ファイルを使用する方法を示しています。 以下のリンクから、ildasm.exeのMicrosoftドキュメントを確認してください。



 // Save the Assembly and generate the MSIL code with ILDASM.EXE string modName = "expression.dll"; Process p = new Process(); p.StartInfo.FileName = "ildasm.exe"; p.StartInfo.Arguments = "/text /nobar \"" + modName; p.StartInfo.UseShellExecute = false; p.StartInfo.CreateNoWindow = true; p.StartInfo.RedirectStandardOutput = true; p.StartInfo.WindowStyle = ProcessWindowStyle.Hidden; p.Start(); string s = p.StandardOutput.ReadToEnd(); p.WaitForExit(); p.Close(); txtMsil.Text = s;
      
      





参照:






All Articles