Delegate.CreateDelegate创建的委托为什么比lambda表达式和方法委托执行速度更快?

5

一直以来,我一直在阅读关于反射的文章,每个人都说:“反射很慢”,“反射很慢”。现在我决定测试一下它到底有多慢,让我惊讶的是,使用反射创建的委托实际上比使用 lambda 创建的委托快了约两倍。而且令人惊讶的是,使用声明方法创建的委托要慢大约四倍。

查看代码

这是一个自定义类,其属性获取方法将用于委托:

#class to test
class SomeClass
{
    public SomeClass A { get; set; } //property to be gotten
    public static SomeClass GetA(SomeClass c) { return c.A; } //declared getter method
}

这是我测试过的三个代表:

PropertyInfo AProp = typeof(SomeClass).GetProperty("A");

//1 - Created with reflection
Func<SomeClass, SomeClass> Getter = (Func<SomeClass, SomeClass>)Delegate.CreateDelegate(typeof(Func<SomeClass, SomeClass>), null, AProp.GetGetMethod());

//2 - Created with a lambda expression
Func<SomeClass, SomeClass> Getter2 = c => c.A;

//3 - Created from a declared method
Func<SomeClass, SomeClass> Getter3 = SomeClass.GetA;

这些是测试:

SomeClass C = new SomeClass();
C.A = new SomeClass(); //test doesn't change whether A is set or null
Stopwatch w;

//reflection delegate
w = Stopwatch.StartNew();
for (int i = 0; i < 10000000; i++) { SomeClass b = Getter(C); }
w.Stop(); Console.WriteLine(w.Elapsed);

//lambda delegate
w = Stopwatch.StartNew();
for (int i = 0; i < 10000000; i++) { SomeClass b = Getter2(C); }
w.Stop(); Console.WriteLine(w.Elapsed);

//method delegate
w = Stopwatch.StartNew();
for (int i = 0; i < 10000000; i++) { SomeClass b = Getter3(C); }
w.Stop(); Console.WriteLine(w.Elapsed);

//no delegate
w = Stopwatch.StartNew();
for (int i = 0; i < 10000000; i++) { SomeClass b = C.A; }
w.Stop(); Console.WriteLine(w.Elapsed);

结果如下:

enter image description here

我还尝试过倒转测试顺序,以查看是否有影响,或者手表是否在欺骗我,但是测试结果是一致的。

编辑:考虑到“发布”编译版本,如建议所示:

enter image description here

现在... 我本来期望 Lambda 要慢一些。


1
我在发布模式下编译时,没有看到lambda和反射性能之间有太大的差异,但是在调试模式下,我看到了与你类似的输出。 - Jonathon Chase
......当然......我没有想到那个。如果你愿意,你可以把它发表为答案。 - Daniel Möller
我无法确切地说出原因和错误所在,但这是我使用BenchmarkDotNet库编写的代码的结果。毫无疑问,基准测试很困难。 - Uladzislaŭ
1
做这种基准测试,我建议您使用BenchMarkDotNet,它可以为您提供更多的见解,以了解您看到这种行为的原因,例如分配、内存使用、GC暂停等。https://github.com/dotnet/BenchmarkDotNet - Pedro
1
你看过这个吗?http://vibrantcode.com/2013/02/19/lambdas-vs-method-groups/ - dnickless
1个回答

1
这是对其进行反编译的结果:
    Func<SomeClass, SomeClass> Getter = (Func<SomeClass, SomeClass>)Delegate.CreateDelegate(typeof(Func<SomeClass, SomeClass>), null, AProp.GetGetMethod());
    Func<SomeClass, SomeClass> arg_51_0;
    if ((arg_51_0 = Program.<>c.<>9__12_0) == null)
    {
        arg_51_0 = (Program.<>c.<>9__12_0 = new Func<SomeClass, SomeClass>(Program.<>c.<>9.<Main>b__12_0));
    }
    Func<SomeClass, SomeClass> Getter2 = arg_51_0;
    Func<SomeClass, SomeClass> Getter3 = new Func<SomeClass, SomeClass>(SomeClass.GetA);

注意第一个几乎未经修改即通过编译,而第二个和第三个则被大幅修改。
如果我要猜测:
第一个调用利用了Delegate库中使用的某种诡秘的C++/COM内存管理技巧。
第二个创建了一个新方法并在调用其新方法之前添加了一个空检查。
而第三个则类似于第二个,但将其保存到运行时,这是我猜测为什么仍然是一个属性调用,在新的内联方法中(我本来期望它会被放到自己的编译器创建的方法中,类似于第二个版本,所以我猜这部分将在编译时发生,这解释了为什么它的时间比前两个要高得多)。
我认为关于反射速度慢的评论更针对大型库;我猜想你在这里看不到它,因为被反映的类非常小,所以没有太多需要反映的。

编辑:当我打出最后一句话时,我决定尝试通过扩展SomeClass对象来减缓第一个调用。我添加了大约30个新属性和20个左右的新方法。似乎没有什么区别。我也听到了有关反射的所有警告,所以这有点令人惊讶。这篇文章指出,所有这些都涉及到一个缓存,这可能会帮助很多。如果所有方法元数据都被缓存,那么反射应该比通过编译器添加的额外方法和检查更快。也许当你反射一个尚未加载/缓存的外部类时,它就会出现。不过这是一个更加复杂的实验。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接