优化由表达式树生成的Func.Invoke()函数

8
我正在进行一个动态实例化类的自动化工作。
我决定编写一个表达式树来生成一个Func,以便能够为我实例化我的类。然而,我注意到使用Func比简单地使用new要慢3倍。
从我了解的表达式树和调用函数的知识来看,性能差异应该几乎不存在(也许只有20-30%,但绝不是3倍之多)。
首先,这是我构建的表达式。
public Expression<Func<A1, T>> BuildLambda<T, A1>(string param1Name)
    {
        var createdType = typeof(T);

        var param = Expression.Parameter(typeof(A1), param1Name);
        var ctor = Expression.New(createdType);
        var prop = createdType.GetProperty(param1Name);

        var displayValueAssignment = Expression.Bind(prop, param);
        var memberInit = Expression.MemberInit(ctor, displayValueAssignment);

        return
            Expression.Lambda<Func<A1, T>>(memberInit, param);
    }

我接下来会这样编译它(我只需要这样做一次)。
var c1 = mapper.BuildLambda<Class1, int>("Id").Compile();

然后我像这样调用我的Func函数
var result = c1.Invoke(5);

当我将这个最后部分放入循环中并与类似的内容进行比较时。
var result = new Class1() { Id = 5 };

我进行了一些测试,比较了两者的性能,以下是我的结论:

100,000    Iterations - new: 0ms.   | Func 2ms.
600,000    Iterations - new: 5ms.   | Func 14ms.
3,100,000  Iterations - new: 24ms.  | Func 74ms.
15,600,000 Iterations - new: 118ms. | Func 378ms.
78,100,000 Iterations - new: 597ms. | Func 1767ms.

如您所见,我的 Func.Invoke() 大约比使用 new 实例化慢2.5-3倍。 有没有人有什么提示可以帮助我改进这个问题?(我不介意使用纯反射,因为我能够获得更好的性能)
*对于任何想要测试此代码的人,请查看我的设置: https://pastebin.com/yvMLqZ2t

3
为什么不直接在问题中包含这个内容?删除未使用的“ExpressionParam”类和命名空间声明后,它只有约60行代码... - Jon Skeet
4
有趣的是,这只是在完整的.NET框架中存在的问题。.NET Core的表现符合预期。 - nejcs
3
手动创建的表达式树的性能与由编译器从Lambda表达式创建的表达式树的性能基本相同。 - Jon Skeet
4
请查看@IvanStoev推荐的第一个问题中的答案。如果您将[assembly: AllowPartiallyTrustedCallers]添加到代码中,则两种变体的结果几乎相同(在我的机器上,表达式调用最多慢1.5倍)。该答案还有一个链接指向另一个问题,其中给出了相同的答案。虽然我不确定原因,但如果有人能深入探讨“这似乎有点错误”的原因,我也会非常感激... - Akos Nagy
显示剩余10条评论
2个回答

2
阅读所有评论后,我想到了这个想法:当您创建 DynamicMethod 而不是表达式树,并将其逻辑地分配给当前执行代码的模块时,您不应该遇到此开销。
我认为(或者至少希望)您正在寻找改进通用想法的选项,而不是特定于基于表达式树的版本,因此我将其作为改进选项发布 :)
所以我尝试了这段代码:
 public static Func<A1, T> BuildLambda<A1, T>(string propertyName)
 {
   // This is where the magic happens with the last parameter!!
   DynamicMethod dm = new DynamicMethod("Create", typeof(T), new Type[] { typeof(A1) }, typeof(Program).Module);

   // Everything else is just generating IL-code at runtime to create the class and set the property
   var setter = typeof(T).GetProperty(propertyName).SetMethod;
   var generator = dm.GetILGenerator();
   var local = generator.DeclareLocal(typeof(T));
   generator.Emit(OpCodes.Newobj, typeof(Class1).GetConstructor(Type.EmptyTypes));
   generator.Emit(OpCodes.Stloc, local);
   generator.Emit(OpCodes.Ldloc, local);
   generator.Emit(OpCodes.Ldarg_0);
   generator.Emit(OpCodes.Call, setter);
   generator.Emit(OpCodes.Ldloc, local);
   generator.Emit(OpCodes.Ret);
   return (Func<A1, T>)dm.CreateDelegate(typeof(Func<A1, T>));
}

在我的机器上,这个生成的委托执行速度最多比手写代码慢1.8倍,而且不需要指定属性。不是1.5倍,但至少我不必在我的代码中包含一个我完全不理解的程序集范围属性:)
请注意,如果省略DynamicMethod构造函数的最后一个参数,生成的代码仍然会更慢。 编辑 我偶然发现了这篇博客文章,它提出了同样的问题并给出了相同的解决方案: https://blogs.msdn.microsoft.com/seteplia/2017/02/01/dissecting-the-new-constraint-in-c-a-perfect-example-of-a-leaky-abstraction/

尝试了一下,我确实成功地使执行速度变慢了1.3-1.6倍。我猜typeof(Program).Module中的“魔法”基本上围绕着这个事实展开,即将该方法作为当前程序集的一部分添加,意味着没有其他程序集需要建立握手过程。(这基本上就是程序集属性帮助解决的问题)。感谢您的建议。如果我找不到更有益的东西,我可能会使用它 :) - Hentov
类似这样。请注意,程序集是不可变的;一旦完成,就不能在物理上添加任何内容。模块是组成程序集的较小代码单元,如果您使用C#编写asm,则无法看到它们。这意味着编译后也无法向模块添加任何内容。文档建议最后一个参数创建逻辑关联。由于它不是物理的,因此不会将其添加到asm本身中,只是从运行时(例如安全性)方面与之链接。也许 :) - Akos Nagy

0

让我尝试一些不同的东西。你可能会做的事情是柯里化:

Func<TArg, TRes> BuildFuncFor<TClass, TArg, TRes>(Func<TClass> typeCreator, Action<TArg, TClass> argumentAssigner) {
        return arg => {
             var type = typeCreator();
             argumentAssigner(arg, type);
             return type;
    }
}

然后,相同的柯里化方法可以应用于提供两个函数的默认/动态实现。一个典型的typeCreator可能是类似于Activator.Create(...)的东西。根据您的逻辑,可能需要更多的函数;例如:Func<object[]> constructorArgumentsSupplier。同样适用于将给定值分配给给定属性:老式反射:就像WPF一样。

但是,它们中的大多数1)可能仅为某种类型创建一次并缓存以供进一步使用;2)预编译而不是依赖于表达式,这有点痛苦。


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