メソッドの呼び出し時間
※http://d.hatena.ne.jp/akiramei/20040518#p1こちらもご覧ください。
C#ではP/InvokeによってUnmanaged関数を簡単に呼び出すことが出来ます。P/Invokeされる関数は恐らくC/C++などで作られているので大抵高速です。ですが、C#からUnmanaged関数を呼び出すコストもあるはずです。資料によると「8命令で高速に呼び出せる」とありますが、これはどの程度高速なのでしょうか?と、いうことで調査してみました。
以下、テストコード。
Reflection.Emitでは上手く行かなかったのでILで代用した関数ポインタを呼び出すアセンブリ(CalcIL.dll)。
.assembly extern mscorlib {} .assembly CalcIL{} .class public CalcIL { .method public static int32 AddIL(native int pfn, int32 x, int32 y) { .maxstack 3 ldarg.1 ldarg.2 ldarg.0 calli unmanaged stdcall int32(int32,int32) ret } }
色々な関数呼び出しの速度を比較するテストモジュール。
using System; using System.Reflection; using System.Reflection.Emit; using System.Runtime.InteropServices; // Emit経由で関数ポインタを呼ぶ為 public interface ICalc { int EmitAdd(IntPtr pf, int x, int y); } class App { const int LOOP = 100000000; [DllImport("kernel32")] public extern static IntPtr LoadLibrary(string lpLibFileName); [DllImport("kernel32")] public extern static bool FreeLibrary(IntPtr hLibModule); [DllImport("kernel32")] public extern static IntPtr GetProcAddress(IntPtr hModule,string lpProcName); [DllImport("Calc.dll", EntryPoint="Add")] public static extern int AddDLL(int x, int y); public static ICalc GetEmitClass() { AssemblyName asnm = new AssemblyName(); asnm.Name = "EmitAssembly"; AssemblyBuilder ab = AppDomain.CurrentDomain.DefineDynamicAssembly( asnm, AssemblyBuilderAccess.Run); ModuleBuilder md = ab.DefineDynamicModule("EmitModule"); // クラスを作成 TypeBuilder tb = md.DefineType("EmitClass", TypeAttributes.Public); // ICalcを実装させる tb.AddInterfaceImplementation(typeof(ICalc)); // EmitAddの実装 MethodBuilder mt = tb.DefineMethod("EmitAdd", MethodAttributes.Virtual | MethodAttributes.Public, typeof(int), new Type {typeof(IntPtr), typeof(int), typeof(int)}); ILGenerator il = mt.GetILGenerator(); // Unmanaged関数呼び出し il.Emit(OpCodes.Ldarg_2); il.Emit(OpCodes.Ldarg_3); il.Emit(OpCodes.Ldarg_1); il.EmitCalli(OpCodes.Calli, CallingConvention.StdCall, typeof(int), new Type {typeof(int), typeof(int)}); il.Emit(OpCodes.Ret); MethodInfo mi = typeof(ICalc).GetMethod("EmitAdd"); // オーバーライド tb.DefineMethodOverride(mt, mi); return tb.CreateType().GetConstructor(Type.EmptyTypes).Invoke(null) as ICalc; } static App() { } public static int AddStatic(int x, int y) { return x + y; } public int AddMember(int x, int y) { return x + y; } public virtual int AddVirtual(int x, int y) { return x + y; } public static void Main() { App app = new App(); // DllImport DateTime t; t = DateTime.Now; for (int i = 0; i < LOOP; ++i) AddDLL(i, i); Console.WriteLine("DLL -" + (DateTime.Now - t)); // Static Method t = DateTime.Now; for (int i = 0; i < LOOP; ++i) AddStatic(i, i); Console.WriteLine("Static -" + (DateTime.Now - t)); // NonVirtual Method t = DateTime.Now; for (int i = 0; i < LOOP; ++i) app.AddMember(i, i); Console.WriteLine("Member -" + (DateTime.Now - t)); // Virtual Method t = DateTime.Now; for (int i = 0; i < LOOP; ++i) app.AddVirtual(i, i); Console.WriteLine("Virtual-" + (DateTime.Now - t)); // LoadLibraryを使う IntPtr hModule = LoadLibrary("Calc.dll"); IntPtr hFunc = GetProcAddress(hModule, "_Add@8"); // Native Method via IL t = DateTime.Now; for (int i = 0; i < LOOP; ++i) CalcIL.AddIL(hFunc, i, i); Console.WriteLine("IL -" + (DateTime.Now - t)); ICalc calc = GetEmitClass(); // Native Method via Emit t = DateTime.Now; for (int i = 0; i < LOOP; ++i) calc.EmitAdd(hFunc, i, i); Console.WriteLine("Emit -" + (DateTime.Now - t)); FreeLibrary(hModule); } } /* 実行結果 DLL -00:00:12.7187500 Static -00:00:00.1093750 Member -00:00:00.0937500 Virtual-00:00:00.8593750 IL -00:00:06.4687500 Emit -00:00:06.6718750
*/
P/Invoke、結構遅いぞ!ILを使うと半分くらいに出来ますが、ここまで頑張らないと駄目なんでしょうか?とは言え、1億回ループの結果なのでよほどのことが無ければ実用上は問題ない気もしますね。(^^;
あと、Reflection.Emitで呼び出せないのが謎です。技術的に無理なのでしょうか・・・
(追記) インスタンスメソッドということを忘れてました。thisの分が計算に入ってない・・・。この前作ったのはグローバル関数だったので勘違いしてしまいました。と言い訳してみるテスト。