Cập nhật: Đã thêm các điểm chuẩn được biên dịch sẵn và lười biên dịch
Cập nhật 2: Hóa ra, tôi sai. Xem bài của Eric Lippert để có câu trả lời đầy đủ và chính xác. Tôi rời khỏi đây vì lợi ích của các số điểm chuẩn
* Cập nhật 3: Đã thêm các điểm chuẩn IL-Emited và Lazy IL-Emited, dựa trên câu trả lời của Mark Gravell cho câu hỏi này .
Theo hiểu biết của tôi, việc sử dụng dynamic
từ khóa không gây ra bất kỳ sự biên dịch bổ sung nào trong thời gian chạy (mặc dù tôi tưởng tượng nó có thể làm như vậy trong các trường hợp cụ thể, tùy thuộc vào loại đối tượng nào đang hỗ trợ các biến động của bạn).
Về hiệu suất, dynamic
vốn đã giới thiệu một số chi phí, nhưng không nhiều như bạn nghĩ. Ví dụ, tôi vừa chạy một điểm chuẩn trông như thế này:
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>));
}
Như bạn có thể thấy từ mã, tôi cố gắng gọi một phương thức no-op đơn giản theo bảy cách khác nhau:
- Gọi phương thức trực tiếp
- Sử dụng
dynamic
- Theo phản xạ
- Sử dụng một
Action
cái đã được biên dịch trước trong thời gian chạy (do đó không bao gồm thời gian biên dịch từ các kết quả).
- Sử dụng một
Action
cái được biên dịch lần đầu tiên là cần thiết, sử dụng biến Lazy không an toàn cho luồng (do đó bao gồm cả thời gian biên dịch)
- Sử dụng phương pháp tạo động được tạo trước khi kiểm tra.
- Sử dụng một phương pháp được tạo động mà ngay lập tức bị lười biếng trong quá trình thử nghiệm.
Mỗi lần được gọi là 1 triệu lần trong một vòng lặp đơn giản. Dưới đây là kết quả thời gian:
Trực tiếp: 3,4248ms
Năng động: 45,0728ms
Phản xạ: 888.4011ms Được
biên dịch trước: 21.9166ms
LazyCompiled: 30.2045ms
ILEmit: 8.4918ms
LazyILEmit: 14.3483ms
Vì vậy, trong khi sử dụng dynamic
từ khóa mất nhiều thời gian hơn so với gọi trực tiếp phương thức, nó vẫn quản lý để hoàn thành thao tác một triệu lần trong khoảng 50 mili giây, làm cho nó nhanh hơn nhiều so với phản xạ. Nếu phương thức chúng ta gọi là cố gắng thực hiện một cái gì đó chuyên sâu, như kết hợp một vài chuỗi với nhau hoặc tìm kiếm một bộ sưu tập cho một giá trị, thì các thao tác đó có thể vượt xa sự khác biệt giữa cuộc gọi trực tiếp và dynamic
cuộc gọi.
Hiệu suất chỉ là một trong nhiều lý do tốt để không sử dụng một cách dynamic
không cần thiết, nhưng khi bạn xử lý dynamic
dữ liệu thực sự , nó có thể cung cấp các lợi thế vượt xa các nhược điểm.
Cập nhật 4
Dựa trên nhận xét của Johnbot, tôi đã chia khu vực Reflection thành bốn bài kiểm tra riêng biệt:
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();
}),
... và đây là kết quả điểm chuẩn:
Vì vậy, nếu bạn có thể xác định trước một phương thức cụ thể mà bạn sẽ cần gọi rất nhiều, việc gọi một đại biểu được lưu trong bộ nhớ cache tham chiếu đến phương thức đó cũng nhanh như gọi chính phương thức đó. Tuy nhiên, nếu bạn cần xác định phương thức nào để gọi giống như bạn sắp gọi nó, thì việc tạo một đại biểu cho nó rất tốn kém.