【问题标题】:Why is casting with dynamic faster than with object为什么动态转换比对象快
【发布时间】:2020-04-23 02:19:08
【问题描述】:

我真的很困惑,拿下面的代码:

[Benchmark]
public TEnum DynamicCast()
{
    return (TEnum)(dynamic)0;
}

[Benchmark]
public TEnum ObjectCast()
{
    return (TEnum)(object)0;
}

[Benchmark]
public TEnum DirectCast()
{
    return (TEnum)0;
}

public enum TEnum
{
    Foo,
    Bar
}

是的,我知道我可以直接将整数转换为给定的 Enum,但这是我实际代码的简化版本,其中包括使用通用扩展的工作。

无论如何,我进行了一些性能测试,因为我很好奇哪个更快。我正在使用BenchmarkDotNet 并看到那里:

BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18362
Intel Core i7-6700HQ CPU 2.60GHz (Skylake), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.1.100
  [Host]     : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT
  DefaultJob : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT


|      Method |       Mean |     Error |    StdDev |     Median |  Gen 0 | Gen 1 | Gen 2 | Allocated |
|------------ |-----------:|----------:|----------:|-----------:|-------:|------:|------:|----------:|
| DynamicCast |  8.2124 ns | 0.0803 ns | 0.0671 ns |  8.2134 ns | 0.0076 |     - |     - |      24 B |
|  ObjectCast | 13.9178 ns | 0.4822 ns | 0.5922 ns | 13.6714 ns | 0.0076 |     - |     - |      24 B |
|  DirectCast |  0.0538 ns | 0.0422 ns | 0.0534 ns |  0.0311 ns |      - |     - |     - |         - |

动态版本实际上更快,而不仅仅是一点点。我多次运行测试,但我无法绕开它。

我查看了编译后的版本,是否有任何优化,但请参阅here

谁能给我解释一下?

【问题讨论】:

  • 你也比较直接演员return (TEnum)0; 和有什么不同吗?我怀疑装箱/拆箱的级别会影响优化,这与隐式/显式类型转换有关。
  • 猜猜这与装箱/拆箱与通过动态直接投射有关
  • @Jonesopolis 这仍然会让我感到惊讶,因为从个人经验来看,使用动态总是有一些开销。在这种情况下,我真的认为它比拳击开销更高......
  • @Turbot 更新了答案,是的,它之所以如此之快,是因为它将编译为 TEnum.Foo
  • @Twenty 对象转换实际上将值装箱和拆箱,动态转换使用 cast 方法的运行时绑定。我已经添加了带有 IL 详细信息的答案,请查看

标签: c# performance object dynamic performance-testing


【解决方案1】:

我相信隐式/显式类型转换会对您的操作产生影响。 DirectCast 是显式转换中最快的 - ObjectCastDynamicCast 正在实现通常包含在结果中的隐式转换。

【讨论】:

  • 因此,在谈论投射时总是更快(忽略直接投射一秒钟),而不仅仅是限于枚举?此外,编译器生成的代码要长得多,对于DynamicCast 来说似乎也更复杂。甚至不应该至少再多几行吗?我想隐式转换比显式转换更容易。
【解决方案2】:

您可以查看 IL 代码以了解其背后的差异。对象投射

public TEnum ObjectCast()
{
    return (TEnum)(object)0;
}

int 的值装箱到object 中,然后拆箱到TEnum 的值中,因为它是值类型

IL_0001: ldc.i4.0
IL_0002: box          [System.Runtime]System.Int32
IL_0007: unbox.any    TestConsoleApp.Test/TEnum
IL_000c: stloc.0      // V_0
IL_000d: br.s         IL_000f

我想这是与其他示例相比执行速度最慢的主要原因。

dynamic 对象转换

public TEnum DynamicCast()
{
    return (TEnum) (dynamic) 0;
}

看起来更复杂

IL_0001: ldsfld       class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Runtime]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, valuetype TestConsoleApp.Test/TEnum>> TestConsoleApp.Test/'<>o__0'::'<>p__0'
IL_0006: brfalse.s    IL_000a
IL_0008: br.s         IL_002f
IL_000a: ldc.i4.s     16 // 0x10
IL_000c: ldtoken      TestConsoleApp.Test/TEnum
IL_0011: call         class [System.Runtime]System.Type [System.Runtime]System.Type::GetTypeFromHandle(valuetype [System.Runtime]System.RuntimeTypeHandle)
IL_0016: ldtoken      TestConsoleApp.Test
IL_001b: call         class [System.Runtime]System.Type [System.Runtime]System.Type::GetTypeFromHandle(valuetype [System.Runtime]System.RuntimeTypeHandle)
IL_0020: call         class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSiteBinder [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.Binder::Convert(valuetype [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpBinderFlags, class [System.Runtime]System.Type, class [System.Runtime]System.Type)
IL_0025: call         class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<!0/*class [System.Runtime]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, valuetype TestConsoleApp.Test/TEnum>*/> class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Runtime]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, valuetype TestConsoleApp.Test/TEnum>>::Create(class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSiteBinder)
IL_002a: stsfld       class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Runtime]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, valuetype TestConsoleApp.Test/TEnum>> TestConsoleApp.Test/'<>o__0'::'<>p__0'
IL_002f: ldsfld       class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Runtime]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, valuetype TestConsoleApp.Test/TEnum>> TestConsoleApp.Test/'<>o__0'::'<>p__0'
IL_0034: ldfld        !0/*class [System.Runtime]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, valuetype TestConsoleApp.Test/TEnum>*/ class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Runtime]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, valuetype TestConsoleApp.Test/TEnum>>::Target
IL_0039: ldsfld       class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Runtime]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, valuetype TestConsoleApp.Test/TEnum>> TestConsoleApp.Test/'<>o__0'::'<>p__0'
IL_003e: ldc.i4.0
IL_003f: box          [System.Runtime]System.Int32
IL_0044: callvirt     instance !2/*valuetype TestConsoleApp.Test/TEnum*/ class [System.Runtime]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, valuetype TestConsoleApp.Test/TEnum>::Invoke(!0/*class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite*/, !1/*object*/)
IL_0049: stloc.0      // V_0
IL_004a: br.s         IL_004c

加载了一个类型信息,然后使用Binder.Convertstatic method.初始化CallSiteBinder的实例然后创建通用CallSiteclass的实例,使用Createstatic call(ldsfld将静态字段的值推入堆栈)。我不是 100% 确定,但通用参数 Func&lt;CallSite, object, TEnum 表示一个函数,将调用该函数将对象转换为 TEnum。最后几行显示此函数绑定到TEnum 类。

因此,在后台编译器已经为您创建了一个将动态对象转换为所需TEnum 类型的方法。并且只有从intobject 的装箱操作才能将其传递给创建的函数。这听起来是一个很好的理由,为什么它比使用装箱和拆箱操作的对象投射更快

【讨论】:

  • 似乎拆箱开销大于 DLR Callsite 缓存。
  • 非常感谢您的解释和深入了解。这似乎也是最合乎逻辑的答案。
  • jit 应该能够优化 box/unbox.any 模式并提供与直接强制转换类似的性能,但如果其中一种类型是枚举而另一种是 int,则会失败。
猜你喜欢
  • 2012-05-15
  • 1970-01-01
  • 2020-02-22
  • 2021-04-18
  • 2016-08-02
  • 1970-01-01
  • 2016-08-18
  • 1970-01-01
相关资源
最近更新 更多