【问题标题】:Is Activating a Struct Without Storing It as a Local Variable Expected to Be Slower than Not Storing It as a Local Variable?激活结构而不将其存储为局部变量会比不将其存储为局部变量要慢吗?
【发布时间】:2019-03-05 01:12:31
【问题描述】:

我在 .NET Core 2.1 中遇到了一个我想了解的性能问题。代码可以在这里找到:

https://github.com/mike-eee/StructureActivation

这是来自BenchmarkDotNet的相关基准代码:

public class Program
{
    static void Main()
    {
        BenchmarkRunner.Run<Program>();
    }

    [Benchmark(Baseline = true)]
    public uint? Activated() => new Structure(100).SomeValue;

    [Benchmark]
    public uint? ActivatedAssignment()
    {
        var selection = new Structure(100);
        return selection.SomeValue;
    }
}

public readonly struct Structure
{
    public Structure(uint? someValue) => SomeValue = someValue;

    public uint? SomeValue { get; }
}

从一开始,我希望Activated更快,因为它不存储局部变量,我一直认为这会导致在定位和保留空间内的空间时性能损失这样做的当前堆栈上下文。

但是,在运行测试时,我得到以下结果:

// * Summary *

BenchmarkDotNet=v0.11.1, OS=Windows 10.0.17134.285 (1803/April2018Update/Redstone4)
Intel Core i7-4820K CPU 3.70GHz (Haswell), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=2.1.402
  [Host]     : .NET Core 2.1.4 (CoreCLR 4.6.26814.03, CoreFX 4.6.26814.02), 64bit RyuJIT
  DefaultJob : .NET Core 2.1.4 (CoreCLR 4.6.26814.03, CoreFX 4.6.26814.02), 64bit RyuJIT


              Method |     Mean |     Error |    StdDev | Scaled |
-------------------- |---------:|----------:|----------:|-------:|
           Activated | 4.700 ns | 0.0128 ns | 0.0107 ns |   1.00 |
 ActivatedAssignment | 3.331 ns | 0.0278 ns | 0.0260 ns |   0.71 |

激活的结构(不存储局部变量)大约慢 30%

作为参考,这里是 ReSharper 的 IL 查看器提供的 IL:

.method /*06000002*/ public hidebysig instance valuetype [System.Runtime/*23000001*/]System.Nullable`1/*0100000E*/<unsigned int32> 
  Activated() cil managed 
{
  .custom /*0C00000C*/ instance void [BenchmarkDotNet/*23000002*/]BenchmarkDotNet.Attributes.BenchmarkAttribute/*0100000D*/::.ctor() 
    = (01 00 01 00 54 02 08 42 61 73 65 6c 69 6e 65 01 ) // ....T..Baseline.
    // property bool 'Baseline' = bool(true)
  .maxstack 1
  .locals /*11000001*/ init (
    [0] valuetype StructureActivation.Structure/*02000003*/ V_0
  )

  // [14 31 - 14 59]
  IL_0000: ldc.i4.s     100 // 0x64
  IL_0002: newobj       instance void valuetype [System.Runtime/*23000001*/]System.Nullable`1/*0100000E*/<unsigned int32>/*1B000001*/::.ctor(!0/*unsigned int32*/)/*0A00000F*/
  IL_0007: newobj       instance void StructureActivation.Structure/*02000003*/::.ctor(valuetype [System.Runtime/*23000001*/]System.Nullable`1/*0100000E*/<unsigned int32>)/*06000005*/
  IL_000c: stloc.0      // V_0
  IL_000d: ldloca.s     V_0
  IL_000f: call         instance valuetype [System.Runtime/*23000001*/]System.Nullable`1/*0100000E*/<unsigned int32> StructureActivation.Structure/*02000003*/::get_SomeValue()/*06000006*/
  IL_0014: ret          

} // end of method Program::Activated

.method /*06000003*/ public hidebysig instance valuetype [System.Runtime/*23000001*/]System.Nullable`1/*0100000E*/<unsigned int32> 
  ActivatedAssignment() cil managed 
{
  .custom /*0C00000D*/ instance void [BenchmarkDotNet/*23000002*/]BenchmarkDotNet.Attributes.BenchmarkAttribute/*0100000D*/::.ctor() 
    = (01 00 00 00 )
  .maxstack 2
  .locals /*11000001*/ init (
    [0] valuetype StructureActivation.Structure/*02000003*/ selection
  )

  // [19 4 - 19 39]
  IL_0000: ldloca.s     selection
  IL_0002: ldc.i4.s     100 // 0x64
  IL_0004: newobj       instance void valuetype [System.Runtime/*23000001*/]System.Nullable`1/*0100000E*/<unsigned int32>/*1B000001*/::.ctor(!0/*unsigned int32*/)/*0A00000F*/
  IL_0009: call         instance void StructureActivation.Structure/*02000003*/::.ctor(valuetype [System.Runtime/*23000001*/]System.Nullable`1/*0100000E*/<unsigned int32>)/*06000005*/

  // [20 4 - 20 31]
  IL_000e: ldloca.s     selection
  IL_0010: call         instance valuetype [System.Runtime/*23000001*/]System.Nullable`1/*0100000E*/<unsigned int32> StructureActivation.Structure/*02000003*/::get_SomeValue()/*06000006*/
  IL_0015: ret          

} // end of method Program::ActivatedAssignment

经检查,Activated 有两个 newobjActivatedAssignment 只有一个,这可能导致两个基准测试之间存在差异。

我的问题是:这是预期的吗?我试图理解为什么代码较少的基准实际上比代码较多的基准要慢。任何能确保我遵循最佳做法的指导/建议将不胜感激。

【问题讨论】:

  • FWIW,“局部变量”可以用 JIT 完全消除。 MSIL 并不直接转化为“性能效率”。
  • 啊,你是说这可能是与 JIT 相关的问题,@user2864740?虽然我为 .NET Core 标记了这个问题(并且结果清楚地显示了使用的运行时),但我已经更新了这个问题以反映这确实发生在 .NET Core 2.1 中。由于 .NET Core 2.1 非常注重性能,这只会增加我对这个问题的怀疑(和困惑)。
  • 很难对这样的代码产生直觉。您看不到的是,在这两种情况下它都非常慢,抖动优化器放弃了标准优化,因为您使用了可变结构类型。单位? (又名Nullable&lt;uint&gt;)很丑,因为它的 HasValue 字段需要分配,优化器举手是因为它无法通过可能的副作用进行推理。非常重要的是,您还可以使用普通的 uint 执行此操作并进行比较,这会让您在考虑在性能关键代码中使用可为空类型时三思而后行。允许内联方法,在其周围放置一个 for 循环。
  • 够有趣的@HansPassant 我注意到如果我在一个我称之为的方法上使用不可为空的结构(而不​​是可空的)会大大加快结果。在我的案例中,似乎任何时候一个可空结构都在运行时,结果都会受到 10ns 的影响。拥有工具/分析来指出这些类型的问题,而不是花费数天时间进行试验/错误,最终不得不勇敢地使用 StackOverflow 来查看是否可以在这里找到任何指针,这将是非常方便的。感谢您的/信息/见解。

标签: c# performance .net-core c#-7.2 benchmarkdotnet


【解决方案1】:

如果您从方法中查看 JITted 程序集,会更清楚发生了什么:

Program.Activated()
L0000: sub rsp, 0x18
L0004: xor eax, eax              // Initialize Structure to {0}
L0006: mov [rsp+0x10], rax       // Store to stack
L000b: mov eax, 0x64             // Load literal 100
L0010: mov edx, 0x1              // Load literal 1
L0015: xor ecx, ecx              // Initialize SomeValue to {0}
L0017: mov [rsp+0x8], rcx        // Store to stack
L001c: lea rcx, [rsp+0x8]        // Load pointer to SomeValue from stack
L0021: mov [rcx], dl             // Set SomeValue.HasValue to 1
L0023: mov [rcx+0x4], eax        // Set SomeValue.Value to 100
L0026: mov rax, [rsp+0x8]        // Load SomeValue's value from stack
L002b: mov [rsp+0x10], rax       // Store it to a different location on stack
L0030: mov rax, [rsp+0x10]       // Return it from that location
L0035: add rsp, 0x18
L0039: ret

Program.ActivatedAssignment()
L0000: push rax
L0001: xor eax, eax              // Initialize SomeValue to {0}
L0003: mov [rsp], rax            // Store to stack
L0007: mov eax, 0x64             // Load literal 100
L000c: mov edx, 0x1              // Load literal 1
L0011: lea rcx, [rsp]            // Load pointer to SomeValue from stack
L0015: mov [rcx], dl             // Set SomeValue.HasValue to 1
L0017: mov [rcx+0x4], eax        // Set SomeValue.Value to 100
L001a: mov rax, [rsp]            // Return SomeValue
L001e: add rsp, 0x8
L0022: ret

显然,Activated() 正在做更多的工作,这就是它变慢的原因。它归结为大量的堆栈改组(所有引用rsp)。我已尽我所能对它们进行了评论,但Activated() 方法有点复杂,因为movs 是多余的。 ActivatedAssigment() 更直接。

最终,您实际上并没有通过省略局部变量来节省堆栈空间。无论您是否给它命名,该变量都必须在某个时候存在。您粘贴的 IL 代码显示了一个局部变量(他们称之为 V_0),它是 C# 编译器创建的临时变量,因为您没有显式创建它。

两者的不同之处在于,带有 temp 变量的版本仅保留一个堆栈槽 (.maxstack 1),它同时用于 Nullable&lt;T&gt;Structure,因此是洗牌。在带有命名变量的版本中,它保留了 2 个插槽 (.maxstack 2)。

具有讽刺意味的是,在为 selection 预先保留局部变量的版本中,JIT 能够消除外部结构并仅处理其嵌入的 Nullable&lt;T&gt;,从而使代码更简洁/更快。

我不确定您能否从这个示例中推断出任何最佳实践,但我认为很容易看出 C# 编译器是性能差异的根源。 JIT 足够聪明,可以对您的结构做正确的事情,但前提是它看起来以某种方式进入。

【讨论】:

  • 哇,感谢您提供翔实而全面的回答!我仍然在拆卸,更不用说 JIT,所以我肯定很欣赏 cmets。看来编译器(Roslyn)是造成这种情况的原因,我剩下的想法是:你觉得这应该在 Roslyn 的回购中提出吗?因此,我想当我说“最佳实践”时,我的目标也是工具和环境指导。例如,如果您知道任何指出此类问题的 Roslyn 分析器,那么将不胜感激。再次感谢!
  • 这是个好问题。让 Roslyn 团队知道在这种情况下他们可以改进 IL 以帮助 JIT,这当然不会有什么坏处。他们能说的最糟糕的是不;)。我想不出有什么工具会在这里先发制人地警告你,但如果你还没有尝试过,我推荐使用 sharplab.io 来探索 C#、IL 和 Asm 之间的关系。
  • 这是我用于您的示例的设置的链接:sharplab.io/…
  • 太棒了!我不知道您可以从网页中获取 JIT。 :) 这正是我有兴趣添加到我的资源中的原因。在挑战 StackOverflow 之前,我还要考虑一个因素。再次感谢。 ?
  • FWIW,我在这里向 Roslyn 的仓库发布了一个问题:github.com/dotnet/roslyn/issues/30284
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-07-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-04-26
  • 1970-01-01
相关资源
最近更新 更多