31

我正在生成一个表达式树,它将属性从源对象映射到目标对象,然后将其编译为 aFunc<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 倍。我不知道为什么会这样。关于这一点的整个想法是在没有繁琐的手动映射的情况下实现最高性能。

当我从 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);
}

可以理解,第二个比手动执行要慢,因为它涉及字典查找和一些对象实例化,但第三个应该与调用的原始委托一样快,并且转换 fromDelegate发生Func在循环之外。

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

我还做了两次基准测试,以确保 JIT 没有干扰。

编辑

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

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

我使用了 Bart de Smet 在该博客文章中描述的 Sons-of-Strike 调试器扩展来转储生成的动态方法的 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 

我不是 IL 的专家,但这看起来很简单,正是你所期望的,不是吗?那为什么这么慢呢?没有奇怪的装箱操作,没有隐藏的实例化,什么都没有。它与上面的表达式树不完全相同,因为现在也有一个null检查right.Complex

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

L_0000: ldarg.1 
L_0001: ldarg.0 
L_0002: callvirt instance int32 ComplexSourceType::get_ID()
L_0007: callvirt instance void ComplexDestinationType::set_ID(int32)
L_000c: ldarg.0 
L_000d: callvirt instance class NestedSourceType ComplexSourceType::get_Complex()
L_0012: brfalse.s L_0040
L_0014: ldarg.0 
L_0015: callvirt instance class NestedSourceType ComplexSourceType::get_Complex()
L_001a: stloc.0 
L_001b: newobj instance void NestedDestinationType::.ctor()
L_0020: stloc.1 
L_0021: ldloc.1 
L_0022: ldloc.0 
L_0023: callvirt instance int32 NestedSourceType::get_ID()
L_0028: callvirt instance void NestedDestinationType::set_ID(int32)
L_002d: ldloc.1 
L_002e: ldloc.0 
L_002f: callvirt instance string NestedSourceType::get_Name()
L_0034: callvirt instance void NestedDestinationType::set_Name(string)
L_0039: ldarg.1 
L_003a: ldloc.1 
L_003b: callvirt instance void ComplexDestinationType::set_Complex(class NestedDestinationType)
L_0040: ldarg.1 
L_0041: ret 

长得跟我一模一样。。

编辑

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

但仍然困扰我的是为什么素数方法在性能与编译的表达式树相同。

再次,我欢迎任何人在该 GitHub 存储库中运行代码以确认我的测量结果并确保我没有发疯:)

4

5 回答 5

20

对于这么大的偷听者来说,这很奇怪。有几件事情需要考虑。首先,VS 编译的代码应用了不同的属性,这些属性可能会影响抖动以进行不同的优化。

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

至于其他更改,需要说明编译的委托有一个未在此处使用的闭包对象,但这意味着这是一个目标委托,它的执行速度可能会慢一些。您会注意到编译后的委托有一个目标对象,并且所有参数都向下移动了一个。

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

你也在发布模式下运行吗?有一个类似的主题,我们在这里查看了这个问题: 为什么从 Expression<Func<>> 创建的 Func<> 比直接声明的 Func<> 慢?

编辑:也可以在这里查看我的答案: DynamicMethod 比编译的 IL 函数慢得多

主要内容是您应该将以下代码添加到您计划在其中创建和调用运行时生成的代码的程序集中。

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

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

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

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

于 2011-03-01T21:23:15.910 回答
3

听起来您遇到了调用开销。但是,无论来源如何,如果您的方法在从已编译的程序集中加载时运行得更快,只需将其编译为程序集并加载即可!请参阅我在为什么从 Expression<Func<>> 创建的 Func<> 比直接声明的 Func<> 慢的答案?有关如何的更多详细信息。

于 2011-03-04T13:54:13.653 回答
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
于 2011-03-04T12:43:17.580 回答
2

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

这个想法是用来ExpressionVisitor遍历表达式并为相应的表达式类型发出 IL。编写自己的 Visitor 来处理已知的表达式子集,并为尚不支持的表达式类型回退到正常状态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 与 814。

这里是完整的代码

于 2015-11-24T11:41:04.773 回答
1

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

关于第三种解决方案:还需要在运行时评估 Lambda 表达式,这也需要时间。而且这不是少数...

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

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

于 2011-02-19T21:32:35.643 回答