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倍。我不知道为什么会这样。而整个想法是实现最大性能,同时避免手动映射的繁琐。当我使用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);
}
第二种方法比手动执行慢是可以理解的,因为它涉及到字典查找和一些对象实例化,但第三种方法应该与手动执行一样快,因为它调用的是原始委托,并且从
Delegate
到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 存储库中的代码,确认我的测量结果,并确保我没有发疯 :)