编译到委托表达式的性能

时间:2011-02-19 19:30:51

标签: c# performance expression-trees dynamically-generated

我正在生成一个表达式树,它将源对象的属性映射到目标对象,然后将其编译为Func<TSource, TDestination, TDestination>并执行。

这是生成的LambdaExpression

的调试视图
.Lambda #Lambda1<System.Func`3[MemberMapper.Benchmarks.Program+ComplexSourceType,MemberMapper.Benchmarks.Program+ComplexDestinationType,MemberMapper.Benchmarks.Program+ComplexDestinationType]>(
    MemberMapper.Benchmarks.Program+ComplexSourceType $right,
    MemberMapper.Benchmarks.Program+ComplexDestinationType $left) {
    .Block(
        MemberMapper.Benchmarks.Program+NestedSourceType $Complex$955332131,
        MemberMapper.Benchmarks.Program+NestedDestinationType $Complex$2105709326) {
        $left.ID = $right.ID;
        $Complex$955332131 = $right.Complex;
        $Complex$2105709326 = .New MemberMapper.Benchmarks.Program+NestedDestinationType();
        $Complex$2105709326.ID = $Complex$955332131.ID;
        $Complex$2105709326.Name = $Complex$955332131.Name;
        $left.Complex = $Complex$2105709326;
        $left
    }
}

清理它将是:

(left, right) =>
{
    left.ID = right.ID;
    var complexSource = right.Complex;
    var complexDestination = new NestedDestinationType();
    complexDestination.ID = complexSource.ID;
    complexDestination.Name = complexSource.Name;
    left.Complex = complexDestination;
    return left;
}

这是映射这些类型的属性的代码:

public class NestedSourceType
{
  public int ID { get; set; }
  public string Name { get; set; }
}

public class ComplexSourceType
{
  public int ID { get; set; }
  public NestedSourceType Complex { get; set; }
}

public class NestedDestinationType
{
  public int ID { get; set; }
  public string Name { get; set; }
}

public class ComplexDestinationType
{
  public int ID { get; set; }
  public NestedDestinationType Complex { get; set; }
}

执行此操作的手动代码为:

var destination = new ComplexDestinationType
{
  ID = source.ID,
  Complex = new NestedDestinationType
  {
    ID = source.Complex.ID,
    Name = source.Complex.Name
  }
};

问题在于,当我编译LambdaExpression并对结果delegate进行基准测试时,它比手动版本慢大约10倍。我不知道为什么会这样。关于这一点的整个想法是最大的性能,而没有手动映射的繁琐。

当我从他的blog post中获取Bart de Smet关于此主题的代码并对计算素数与编译表达式树的手动版本进行基准测试时,它们的性能完全相同。

LambdaExpression的调试视图看起来像您期望的那样时,会产生这种巨大差异的原因是什么?

修改

根据要求,我添加了我使用的基准:

public static ComplexDestinationType Foo;

static void Benchmark()
{

  var mapper = new DefaultMemberMapper();

  var map = mapper.CreateMap(typeof(ComplexSourceType),
                             typeof(ComplexDestinationType)).FinalizeMap();

  var source = new ComplexSourceType
  {
    ID = 5,
    Complex = new NestedSourceType
    {
      ID = 10,
      Name = "test"
    }
  };

  var sw = Stopwatch.StartNew();

  for (int i = 0; i < 1000000; i++)
  {
    Foo = new ComplexDestinationType
    {
      ID = source.ID + i,
      Complex = new NestedDestinationType
      {
        ID = source.Complex.ID + i,
        Name = source.Complex.Name
      }
    };
  }

  sw.Stop();

  Console.WriteLine(sw.Elapsed);

  sw.Restart();

  for (int i = 0; i < 1000000; i++)
  {
    Foo = mapper.Map<ComplexSourceType, ComplexDestinationType>(source);
  }

  sw.Stop();

  Console.WriteLine(sw.Elapsed);

  var func = (Func<ComplexSourceType, ComplexDestinationType, ComplexDestinationType>)
             map.MappingFunction;

  var destination = new ComplexDestinationType();

  sw.Restart();

  for (int i = 0; i < 1000000; i++)
  {
    Foo = func(source, new ComplexDestinationType());
  }

  sw.Stop();

  Console.WriteLine(sw.Elapsed);
}

第二个可以理解地比手动操作慢,因为它涉及字典查找和一些对象实例化,但第三个应该与它被调用的原始委托一样快,并且{{1 }} Delegate发生在循环之外。

我也尝试将手动代码包装在一个函数中,但我记得它并没有产生显着的差异。无论哪种方式,函数调用都不应该增加一个数量级的开销。

我也做了两次基准测试,以确保JIT不会干扰。

修改

您可以在此处获取此项目的代码:

https://github.com/JulianR/MemberMapper/

我使用了Bart de Smet博客文章中描述的Sons-of-Strike调试器扩展来转储动态方法生成的IL:

Func

我不是IL的专家,但这看起来非常直接,正是你所期待的,不是吗?那它为什么这么慢?没有奇怪的拳击操作,没有隐藏的实例,没有。它与上面的表达式树不完全相同,因为现在还IL_0000: ldarg.2 IL_0001: ldarg.1 IL_0002: callvirt 6000003 ComplexSourceType.get_ID() IL_0007: callvirt 6000004 ComplexDestinationType.set_ID(Int32) IL_000c: ldarg.1 IL_000d: callvirt 6000005 ComplexSourceType.get_Complex() IL_0012: brfalse IL_0043 IL_0017: ldarg.1 IL_0018: callvirt 6000006 ComplexSourceType.get_Complex() IL_001d: stloc.0 IL_001e: newobj 6000007 NestedDestinationType..ctor() IL_0023: stloc.1 IL_0024: ldloc.1 IL_0025: ldloc.0 IL_0026: callvirt 6000008 NestedSourceType.get_ID() IL_002b: callvirt 6000009 NestedDestinationType.set_ID(Int32) IL_0030: ldloc.1 IL_0031: ldloc.0 IL_0032: callvirt 600000a NestedSourceType.get_Name() IL_0037: callvirt 600000b NestedDestinationType.set_Name(System.String) IL_003c: ldarg.2 IL_003d: ldloc.1 IL_003e: callvirt 600000c ComplexDestinationType.set_Complex(NestedDestinationType) IL_0043: ldarg.2 IL_0044: ret 检查了null

这是手动版本的代码(通过Reflector获得):

right.Complex

看起来和我相同..

修改

我按照Michael B关于这个主题的回答中的链接。我尝试在接受的答案中实现这个技巧,它有效!如果你想要一个技巧的摘要:它创建一个动态程序集,并将表达式树编译成该程序集中的静态方法,并且由于某种原因,速度提高了10倍。这样做的一个缺点是我的基准类是内部的(实际上,嵌套在内部的公共类)并且当我试图访问它们时它抛出异常,因为它们不可访问。似乎没有解决方法,但我可以简单地检测引用的类型是否是内部的,并决定使用哪种编译方法。

但仍然让我感到困惑的是为什么素数方法 在性能上与编译的表达式树完全相同。

再次,我欢迎任何人在GitHub存储库中运行代码来确认我的测量并确保我不会疯狂:)

5 个答案:

答案 0 :(得分:19)

对于如此巨大的无意中听到这一点非常奇怪。有几点需要考虑。首先,VS编译代码应用了不同的属性,可能会影响抖动以进行不同的优化。

您是否在这些结果中包含了已编译委托的第一次执行?你不应该,你应该忽略任何代码路径的第一次执行。您还应该将普通代码转换为委托,因为委托调用比调用实例方法稍慢,这比调用静态方法慢。

至于其他更改,有一些事情可以解释这样一个事实,即编译的委托有一个闭包对象,这个对象在这里没有使用,但意味着这是一个目标委托,它可能执行得慢一点。您会注意到已编译的委托具有目标对象,并且所有参数都向下移动了一个。

由lcg生成的方法也被认为是静态的,由于寄存器切换业务,在编译为委托时比实例方法更慢。 (Duffy说“this”指针在CLR中有一个保留寄存器,当你有一个静态委托时,它必须转移到一个不同的寄存器,调用一点点开销)。 最后,在运行时生成的代码似乎比VS生成的代码运行稍慢。在运行时生成的代码似乎有额外的沙盒并从不同的程序集启动(尝试使用像ldftn操作码或calli操作码之类的东西,如果你不相信我,那些reflection.emited委托将编译但不会让你实际执行它们)调用最小的开销。

你还在发布模式下运行吗? 有一个类似的话题我们在这里查看这个问题: Why is Func<> created from Expression<Func<>> slower than Func<> declared directly?

编辑: 另见我的回答: DynamicMethod is much slower than compiled IL function

主要的一点是,您应该将以下代码添加到您计划创建的程序集并调用运行时生成的代码。

[assembly: AllowPartiallyTrustedCallers]
[assembly: SecurityTransparent]
[assembly: SecurityRules(SecurityRuleSet.Level2,SkipVerificationInFullTrust=true)]

始终使用内置委托类型或带有这些标志的程序集中的类型。

原因是匿名动态代码托管在始终标记为部分信任的程序集中。通过允许部分信任的呼叫者,您可以跳过部分握手。透明性意味着您的代码不会提高安全级别(即行为缓慢),最后真正的技巧是调用托管在标记为跳过验证的程序集中的委托类型。 Func<int,int>#Invoke完全受信任,因此无需验证。这将为您提供从VS编译器生成的代码的性能。通过不使用这些属性,您正在考虑.NET 4中的开销。您可能认为SecurityRuleSet.Level1是避免此开销的好方法,但切换安全模型也很昂贵。

简而言之,添加这些属性,然后您的微循环性能测试将运行大致相同。

答案 1 :(得分:3)

听起来你正在遇到调用开销。但是,无论来源如何,如果从编译的程序集加载时方法运行得更快,只需将其编译为程序集并加载即可!有关详细信息,请参阅Why is Func<> created from Expression<Func<>> slower than Func<> declared directly?上的答案。

答案 2 :(得分:2)

检查这些链接,看看编译LambdaExpression时会发生什么(是的,使用反射完成)

  1. http://msdn.microsoft.com/en-us/magazine/cc163759.aspx#S3
  2. http://blogs.msdn.com/b/ericgu/archive/2004/03/19/92911.aspx

答案 3 :(得分:2)

您可以通过Reflection.Emit手动编译表达式树。它通常会提供更快的编译时间(在我的情况下低于约30倍),并允许您调整发出的结果性能。并不是很难做到,特别是如果你的表达式是有限的已知子集。

这个想法是使用ExpressionVisitor遍历表达式并为相应的表达式类型发出IL。它也是#34;相当&#34;很容易编写自己的访问者来处理已知的表达式子集,对于尚未支持的表达式类型>回退到正常Expression.Compile

在我的情况下,我正在生成代理:

Func<object[], object> createA = state =>
    new A(
        new B(), 
        (string)state[11], 
        new ID[2] { new D1(), new D2() }) { 
        Prop = new P(new B()), Bop = new B() 
    };

测试创建相应的表达式树,并比较其Expression.Compile与访问和发出IL,然后从DynamicMethod创建委托。

结果:

  

编译表达式3000次:814
  调用编译表达式5000000次:724
  表达式发出3000次:36
  运行发射的表达式5000000次:722

手动编译时

36 vs 814。

这里是full code

答案 4 :(得分:1)

我认为这就是反思的影响。第二种方法是使用反射来获取和设置值。据我所知,在这一点上,它不是代表,而是花费时间的反思。

关于第三个解决方案:还需要在运行时评估Lambda表达式,这也需要花费时间。那不是很少......

因此,您永远不会像手动复制一样快速地获得第二和第三个解决方案。

在这里查看我的代码示例。如果您不想手动编码,可以认为这是您可以采取的禁食解决方案:http://jachman.wordpress.com/2006/08/22/2000-faster-using-dynamic-method-calls/