在不将结构存储为局部变量的情况下激活结构是否预期比不将其存储为局部变量更慢?
Is Activating a Struct Without Storing It as a Local Variable Expected to Be Slower than Not Storing It as a Local Variable?
我在 .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
会 更快 因为它不存储局部变量,我一直认为这会导致性能损失在当前堆栈上下文中找到并保留 space。
但是,当 运行 测试时,我得到以下结果:
// * 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
只有一个,这可能是造成两个基准之间差异的原因。
我的问题是:这是预期的吗?我试图理解为什么代码较少的基准实际上比代码较多的基准慢。非常感谢任何 guidance/recommendations 确保我遵循最佳实践。
如果您从方法中查看 JIT 程序集,会更清楚发生了什么:
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()
方法由于冗余的 mov
有点令人费解。 ActivatedAssigment()
更直接。
最终,您实际上并没有通过省略局部变量来节省堆栈 space。无论您是否给它命名,该变量都必须在某个时刻存在。您粘贴的 IL 代码显示了一个局部变量(他们称之为 V_0
),这是 C# 编译器创建的临时变量,因为您没有明确创建它。
两者的不同之处在于带有 temp 变量的版本仅保留一个堆栈槽 (.maxstack 1
),并且它同时用于 Nullable<T>
和 Structure
,因此洗牌。在带有命名变量的版本中,它保留了 2 个插槽 (.maxstack 2
).
具有讽刺意味的是,在为selection
预留局部变量的版本中,JIT能够消除外部结构并只处理其嵌入的Nullable<T>
,使得cleaner/faster代码。
我不确定您是否可以从此示例中推断出任何最佳实践,但我认为很容易看出 C# 编译器是性能差异的根源。 JIT 足够聪明,可以对您的结构做正确的事情,但前提是它看起来以某种方式进入。
我在 .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
会 更快 因为它不存储局部变量,我一直认为这会导致性能损失在当前堆栈上下文中找到并保留 space。
但是,当 运行 测试时,我得到以下结果:
// * 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
只有一个,这可能是造成两个基准之间差异的原因。
我的问题是:这是预期的吗?我试图理解为什么代码较少的基准实际上比代码较多的基准慢。非常感谢任何 guidance/recommendations 确保我遵循最佳实践。
如果您从方法中查看 JIT 程序集,会更清楚发生了什么:
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()
方法由于冗余的 mov
有点令人费解。 ActivatedAssigment()
更直接。
最终,您实际上并没有通过省略局部变量来节省堆栈 space。无论您是否给它命名,该变量都必须在某个时刻存在。您粘贴的 IL 代码显示了一个局部变量(他们称之为 V_0
),这是 C# 编译器创建的临时变量,因为您没有明确创建它。
两者的不同之处在于带有 temp 变量的版本仅保留一个堆栈槽 (.maxstack 1
),并且它同时用于 Nullable<T>
和 Structure
,因此洗牌。在带有命名变量的版本中,它保留了 2 个插槽 (.maxstack 2
).
具有讽刺意味的是,在为selection
预留局部变量的版本中,JIT能够消除外部结构并只处理其嵌入的Nullable<T>
,使得cleaner/faster代码。
我不确定您是否可以从此示例中推断出任何最佳实践,但我认为很容易看出 C# 编译器是性能差异的根源。 JIT 足够聪明,可以对您的结构做正确的事情,但前提是它看起来以某种方式进入。