gpt4 book ai didi

c# - 编译委托(delegate)表达式的性能

转载 作者:IT王子 更新时间:2023-10-29 04:09:45 26 4
gpt4 key购买 nike

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

这是结果 LambdaExpression 的调试 View :

.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 的 blog post 中获取代码时在这个主题上,将计算素数的手动版本与编译表达式树进行基准测试,它们在性能上完全相同。

LambdaExpression 的调试 View 时,是什么导致了这种巨大的差异?看起来像您期望的那样?

编辑

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

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)和来自 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 倍。这样做的缺点是我的基准类是内部的(实际上,公共(public)类嵌套在内部类中)并且当我试图访问它们时抛出异常,因为它们不可访问。似乎没有解决方法,但我可以简单地检测引用的类型是否是内部类型,并决定使用哪种编译方法。

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

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

最佳答案

听到这么大的声音真是太奇怪了。有几件事需要考虑。首先,VS 编译代码应用了不同的属性,这些属性可能会影响抖动以不同方式进行优化。

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

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

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

你也在 Release模式下运行吗?有一个类似的话题,我们在这里查看了这个问题: 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)]

并且始终使用内置委托(delegate)类型或来自带有这些标志的程序集的委托(delegate)类型。

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

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

关于c# - 编译委托(delegate)表达式的性能,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/5053032/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com