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