Jul*_*anR 31 c# performance expression-trees dynamically-generated
我正在生成一个表达式树,它将属性从源对象映射到目标对象,然后编译为a 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
}
}
Run Code Online (Sandbox Code Playgroud)
清理它将是:
(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;
}
Run Code Online (Sandbox Code Playgroud)
这是映射这些类型的属性的代码:
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; }
}
Run Code Online (Sandbox Code Playgroud)
执行此操作的手动代码是:
var destination = new ComplexDestinationType
{
ID = source.ID,
Complex = new NestedDestinationType
{
ID = source.Complex.ID,
Name = source.Complex.Name
}
};
Run Code Online (Sandbox Code Playgroud)
问题是,当我编译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);
}
Run Code Online (Sandbox Code Playgroud)
第二个可以理解地比手动操作要慢,因为它涉及字典查找和一些对象实例化,但第三个应该和它的原始委托一样快,它被调用并且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
Run Code Online (Sandbox Code Playgroud)
我不是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
Run Code Online (Sandbox Code Playgroud)
看起来和我一样..
编辑
我按照Michael B关于这个主题的回答中的链接.我尝试在接受的答案中实现这个技巧,它有效!如果你想要一个技巧的摘要:它创建一个动态程序集,并将表达式树编译成该程序集中的静态方法,并且由于某种原因,速度提高了10倍.这样做的一个缺点是我的基准类是内部的(实际上,嵌套在内部的公共类)并且当我试图访问它们时它抛出异常,因为它们不可访问.似乎没有解决方法,但我可以简单地检测引用的类型是否是内部的,并决定使用哪种编译方法.
还有什么错误我虽然是为什么素数的方法是在性能上编译表达式树是相同的.
再次,我欢迎任何人在GitHub存储库运行代码以确认我的测量并确保我不疯狂:)
Mic*_*l B 19
对于如此巨大的无意中听到这一点非常奇怪.有几点需要考虑.首先,VS编译代码具有不同的属性,可能会影响抖动以进行不同的优化.
您是否在这些结果中包含了已编译委托的第一次执行?你不应该,你应该忽略任何代码路径的第一次执行.您还应该将普通代码转换为委托,因为委托调用比调用实例方法稍慢,这比调用静态方法慢.
至于其他更改,有一些事情可以解释这样一个事实,即编译的委托有一个闭包对象,这里没有使用,但这意味着这是一个目标委托,它可能执行得慢一点.您会注意到已编译的委托具有目标对象,并且所有参数都向下移动了一个.
此外,由lcg生成的方法被认为是静态的,由于寄存器切换业务,在编译为委托时往往比实例方法慢.(Duffy说"this"指针在CLR中有一个保留寄存器,当你有一个静态委托时,它必须转移到一个不同的寄存器,调用一点点开销).最后,在运行时生成的代码似乎比VS生成的代码运行稍慢.在运行时生成的代码似乎有额外的沙盒并从不同的程序集启动(尝试使用像ldftn操作码或calli操作码之类的东西,如果你不相信我,那些reflection.emited委托将编译但不会让你实际执行它们)调用最小的开销.
你还在发布模式下运行吗?我们在这里查看了一个类似的主题: 为什么从Expression <Func <>>创建的Func <>比直接声明的Func <>慢?
编辑:另请参阅我的答案: DynamicMethod比编译的IL函数慢得多
主要的一点是,您应该将以下代码添加到计划创建的程序集并调用运行时生成的代码.
[assembly: AllowPartiallyTrustedCallers]
[assembly: SecurityTransparent]
[assembly: SecurityRules(SecurityRuleSet.Level2,SkipVerificationInFullTrust=true)]
Run Code Online (Sandbox Code Playgroud)
并始终使用内置委托类型或具有这些标志的程序集中的类型.
原因是匿名动态代码托管在始终标记为部分信任的程序集中.通过允许部分信任的呼叫者,您可以跳过部分握手.透明性意味着您的代码不会提高安全级别(即行为缓慢),最后真正的诀窍是调用托管在标记为跳过验证的程序集中的委托类型.Func<int,int>#Invoke完全信任,因此无需验证.这将为您提供从VS编译器生成的代码的性能.通过不使用这些属性,您正在考虑.NET 4中的开销.您可能认为SecurityRuleSet.Level1是避免此开销的好方法,但切换安全模型也很昂贵.
简而言之,添加这些属性,然后你的微循环性能测试,将运行大致相同.