Pembaruan: Menambahkan tolok ukur yang dikompilasi sebelumnya dan yang dikompilasi malas
Pembaruan 2: Ternyata, saya salah. Lihat posting Eric Lippert untuk jawaban yang lengkap dan benar. Saya meninggalkan ini di sini demi nomor benchmark
* Pembaruan 3: Menambahkan tolok ukur IL-Emitted dan Lazy IL-Emitted, berdasarkan jawaban Mark Gravell untuk pertanyaan ini .
Sepengetahuan saya, penggunaan dynamic
kata kunci tidak menyebabkan kompilasi tambahan saat runtime di dalam dan dari dirinya sendiri (meskipun saya membayangkan itu bisa melakukannya dalam keadaan tertentu, tergantung pada jenis objek apa yang mendukung variabel dinamis Anda).
Mengenai kinerja, dynamic
memang secara inheren memperkenalkan beberapa overhead, tetapi tidak sebanyak yang Anda pikirkan. Misalnya, saya baru saja menjalankan patokan yang terlihat seperti ini:
void Main()
{
Foo foo = new Foo();
var args = new object[0];
var method = typeof(Foo).GetMethod("DoSomething");
dynamic dfoo = foo;
var precompiled =
Expression.Lambda<Action>(
Expression.Call(Expression.Constant(foo), method))
.Compile();
var lazyCompiled = new Lazy<Action>(() =>
Expression.Lambda<Action>(
Expression.Call(Expression.Constant(foo), method))
.Compile(), false);
var wrapped = Wrap(method);
var lazyWrapped = new Lazy<Func<object, object[], object>>(() => Wrap(method), false);
var actions = new[]
{
new TimedAction("Direct", () =>
{
foo.DoSomething();
}),
new TimedAction("Dynamic", () =>
{
dfoo.DoSomething();
}),
new TimedAction("Reflection", () =>
{
method.Invoke(foo, args);
}),
new TimedAction("Precompiled", () =>
{
precompiled();
}),
new TimedAction("LazyCompiled", () =>
{
lazyCompiled.Value();
}),
new TimedAction("ILEmitted", () =>
{
wrapped(foo, null);
}),
new TimedAction("LazyILEmitted", () =>
{
lazyWrapped.Value(foo, null);
}),
};
TimeActions(1000000, actions);
}
class Foo{
public void DoSomething(){}
}
static Func<object, object[], object> Wrap(MethodInfo method)
{
var dm = new DynamicMethod(method.Name, typeof(object), new Type[] {
typeof(object), typeof(object[])
}, method.DeclaringType, true);
var il = dm.GetILGenerator();
if (!method.IsStatic)
{
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Unbox_Any, method.DeclaringType);
}
var parameters = method.GetParameters();
for (int i = 0; i < parameters.Length; i++)
{
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldc_I4, i);
il.Emit(OpCodes.Ldelem_Ref);
il.Emit(OpCodes.Unbox_Any, parameters[i].ParameterType);
}
il.EmitCall(method.IsStatic || method.DeclaringType.IsValueType ?
OpCodes.Call : OpCodes.Callvirt, method, null);
if (method.ReturnType == null || method.ReturnType == typeof(void))
{
il.Emit(OpCodes.Ldnull);
}
else if (method.ReturnType.IsValueType)
{
il.Emit(OpCodes.Box, method.ReturnType);
}
il.Emit(OpCodes.Ret);
return (Func<object, object[], object>)dm.CreateDelegate(typeof(Func<object, object[], object>));
}
Seperti yang dapat Anda lihat dari kode, saya mencoba memanggil metode no-op sederhana dengan tujuh cara berbeda:
- Panggilan metode langsung
- Menggunakan
dynamic
- Dengan refleksi
- Menggunakan
Action
yang dikompilasi pada saat runtime (sehingga tidak termasuk waktu kompilasi dari hasil).
- Menggunakan sebuah
Action
yang dikompilasi pertama kali dibutuhkan, menggunakan variabel Malas non-thread-safe (dengan demikian termasuk waktu kompilasi)
- Menggunakan metode yang dihasilkan secara dinamis yang akan dibuat sebelum tes.
- Menggunakan metode yang dihasilkan secara dinamis yang menjadi malas dipakai selama tes.
Masing-masing dipanggil 1 juta kali dalam satu loop sederhana. Berikut adalah hasil waktunya:
Langsung: 3,4248ms
Dinamis: 45,0728 ms
Refleksi: 888,4011ms
Prekompilasi: 21,9166ms
MalasDilengkapi: 30,2045ms
ILEmitted: 8,4918ms
MalasILEmitted: 14,3483ms
Jadi, sementara menggunakan dynamic
kata kunci membutuhkan urutan lebih lama daripada memanggil metode secara langsung, masih berhasil menyelesaikan operasi sejuta kali dalam sekitar 50 milidetik, membuatnya jauh lebih cepat daripada refleksi. Jika metode yang kami panggil sedang mencoba melakukan sesuatu yang intensif, seperti menggabungkan beberapa string bersama-sama atau mencari koleksi untuk suatu nilai, operasi-operasi itu mungkin akan jauh lebih besar daripada perbedaan antara panggilan langsung dan dynamic
panggilan.
Kinerja hanyalah salah satu dari banyak alasan bagus untuk tidak menggunakan yang dynamic
tidak perlu, tetapi ketika Anda berurusan dengan dynamic
data yang sebenarnya , itu dapat memberikan keuntungan yang jauh lebih besar daripada kerugiannya.
Perbarui 4
Berdasarkan komentar Johnbot, saya membagi area Refleksi menjadi empat tes terpisah:
new TimedAction("Reflection, find method", () =>
{
typeof(Foo).GetMethod("DoSomething").Invoke(foo, args);
}),
new TimedAction("Reflection, predetermined method", () =>
{
method.Invoke(foo, args);
}),
new TimedAction("Reflection, create a delegate", () =>
{
((Action)method.CreateDelegate(typeof(Action), foo)).Invoke();
}),
new TimedAction("Reflection, cached delegate", () =>
{
methodDelegate.Invoke();
}),
... dan berikut adalah hasil benchmark:
Jadi, jika Anda dapat menentukan sebelumnya metode tertentu yang harus Anda panggil banyak, memohon delegasi yang di-cache merujuk pada metode itu adalah secepat memanggil metode itu sendiri. Namun, jika Anda perlu menentukan metode mana yang harus dihubungi saat Anda akan memintanya, membuat delegasi untuk itu sangat mahal.