ReadOnlySpan<T> 参数应该使用 "in" 修饰符吗?
Should ReadOnlySpan<T> parameters use the "in" modifier?
C# 7.2 引入了 reference semantics with value-types, and alongside this Microsoft have developed types like Span<T>
and ReadOnlySpan<T>
以潜在地提高需要在连续内存区域上执行操作的应用程序的性能。
根据文档,一种可能提高性能的方法是通过向这些类型的参数添加 in
修饰符来通过引用传递不可变结构:
void PerformOperation(in SomeReadOnlyStruct value)
{
}
我想知道我是否应该对 ReadOnlySpan<T>
这样的类型执行此操作。我是否应该像这样声明接受只读跨度的方法:
void PerformOperation<T>(in ReadOnlySpan<T> value)
{
}
或简单地喜欢:
void PerformOperation<T>(ReadOnlySpan<T> value)
{
}
前者会比后者提供任何性能优势吗?我找不到任何在这两个方向上明确建议的文档,但我确实找到了 this example 他们演示了一种接受 ReadOnlySpan
而 没有 使用的方法in
修饰符。
这里的一个关键因素是规模; Span<T>
/ ReadOnlySpan<T>
故意 非常小 ,因此跨度和 reference-to-a-span 之间的差异很小。 in
的一个关键用法是用于 larger 只读结构,以避免大量的堆栈复制;请注意,有一个 trade-off:in
实际上是一个 ref
,因此您要为所有访问添加一个额外的间接层,除非 JIT 看到您在做什么并起作用一些巫术。当然:如果类型 没有 将自己声明为 readonly
则堆栈副本会自动添加到 before 调用 preserve语义。
Marc 的回答似乎 spot-on。我发布这个只是为了用一些基准来补充他自己的答案,这些基准证实了他所说的话。
我设置了以下基准 class:
public class SpanBenchmarks
{
private const int Iterations = 100_000;
private byte[] _data;
private LargeStruct _control;
[GlobalSetup]
public void GlobalSetup()
{
_data = new byte[1000];
new Random().NextBytes(_data);
_control = new LargeStruct(_data[0], _data[1], _data[2], _data[3], _data[4], _data[5]);
}
[Benchmark]
public void PassSpanByValue()
{
for (int i = 0; i < Iterations; i++) AcceptSpanByValue(_data);
}
[Benchmark]
public void PassSpanByRef()
{
for (int i = 0; i < Iterations; i++) AcceptSpanByRef(_data);
}
[Benchmark]
public void PassLargeStructByValue()
{
for (int i = 0; i < Iterations; i++) AcceptLargeStructByValue(_control);
}
[Benchmark]
public void PassLargeStructByRef()
{
for (int i = 0; i < Iterations; i++) AcceptLargeStructByRef(_control);
}
private int AcceptSpanByValue(ReadOnlySpan<byte> span) => span.Length;
private int AcceptSpanByRef(in ReadOnlySpan<byte> span) => span.Length;
private decimal AcceptLargeStructByValue(LargeStruct largeStruct) => largeStruct.A;
private decimal AcceptLargeStructByRef(in LargeStruct largeStruct) => largeStruct.A;
private readonly struct LargeStruct
{
public LargeStruct(decimal a, decimal b, decimal c, decimal d, decimal e, decimal f)
{
A = a;
B = b;
C = c;
D = d;
E = e;
F = f;
}
public decimal A { get; }
public decimal B { get; }
public decimal C { get; }
public decimal D { get; }
public decimal E { get; }
public decimal F { get; }
}
}
我用这个重复了三次相同的基准测试,每次都得到了相似的结果:
BenchmarkDotNet=v0.10.13, OS=Windows 10 Redstone 3 [1709, Fall Creators Update] (10.0.16299.248)
Intel Core i7-4790 CPU 3.60GHz (Haswell), 1 CPU, 8 logical cores and 4 physical cores
Frequency=3507500 Hz, Resolution=285.1033 ns, Timer=TSC
.NET Core SDK=2.1.300-preview2-008354
[Host] : .NET Core 2.0.6 (CoreCLR 4.6.26212.01, CoreFX 4.6.26212.01), 64bit RyuJIT
DefaultJob : .NET Core 2.0.6 (CoreCLR 4.6.26212.01, CoreFX 4.6.26212.01), 64bit RyuJIT
Method | Mean | Error | StdDev |
----------------------- |----------:|----------:|----------:|
PassSpanByValue | 641.71 us | 0.1758 us | 0.1644 us |
PassSpanByRef | 642.62 us | 0.1524 us | 0.1190 us |
PassLargeStructByValue | 390.78 us | 0.2633 us | 0.2463 us |
PassLargeStructByRef | 35.33 us | 0.3446 us | 0.3055 us |
使用大型结构作为控件,我确认通过引用而不是通过值传递它们具有显着的性能优势。但是,通过引用或值传递 Span<T>
之间没有显着的性能差异。
2019 年 9 月更新
出于好奇,我 运行 再次使用 .NET Core 2.2 进行相同的基准测试。自上次以来似乎引入了一些巧妙的优化,以减少将数组隐式转换为 Span<T>
:
的开销
BenchmarkDotNet=v0.11.5, OS=Windows 10.0.17134.984 (1803/April2018Update/Redstone4)
Intel Core i7-4700HQ CPU 2.40GHz (Haswell), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=2.2.106
[Host] : .NET Core 2.2.4 (CoreCLR 4.6.27521.02, CoreFX 4.6.27521.01), 64bit RyuJIT
DefaultJob : .NET Core 2.2.4 (CoreCLR 4.6.27521.02, CoreFX 4.6.27521.01), 64bit RyuJIT
| Method | Mean | Error | StdDev |
|----------------------- |----------:|----------:|----------:|
| PassSpanByValue | 39.78 us | 0.1873 us | 0.1660 us |
| PassSpanByRef | 41.21 us | 0.2618 us | 0.2186 us |
| PassLargeStructByValue | 475.41 us | 1.3104 us | 1.0943 us |
| PassLargeStructByRef | 39.75 us | 0.1001 us | 0.0937 us |
C# 7.2 引入了 reference semantics with value-types, and alongside this Microsoft have developed types like Span<T>
and ReadOnlySpan<T>
以潜在地提高需要在连续内存区域上执行操作的应用程序的性能。
根据文档,一种可能提高性能的方法是通过向这些类型的参数添加 in
修饰符来通过引用传递不可变结构:
void PerformOperation(in SomeReadOnlyStruct value)
{
}
我想知道我是否应该对 ReadOnlySpan<T>
这样的类型执行此操作。我是否应该像这样声明接受只读跨度的方法:
void PerformOperation<T>(in ReadOnlySpan<T> value)
{
}
或简单地喜欢:
void PerformOperation<T>(ReadOnlySpan<T> value)
{
}
前者会比后者提供任何性能优势吗?我找不到任何在这两个方向上明确建议的文档,但我确实找到了 this example 他们演示了一种接受 ReadOnlySpan
而 没有 使用的方法in
修饰符。
这里的一个关键因素是规模; Span<T>
/ ReadOnlySpan<T>
故意 非常小 ,因此跨度和 reference-to-a-span 之间的差异很小。 in
的一个关键用法是用于 larger 只读结构,以避免大量的堆栈复制;请注意,有一个 trade-off:in
实际上是一个 ref
,因此您要为所有访问添加一个额外的间接层,除非 JIT 看到您在做什么并起作用一些巫术。当然:如果类型 没有 将自己声明为 readonly
则堆栈副本会自动添加到 before 调用 preserve语义。
Marc 的回答似乎 spot-on。我发布这个只是为了用一些基准来补充他自己的答案,这些基准证实了他所说的话。
我设置了以下基准 class:
public class SpanBenchmarks
{
private const int Iterations = 100_000;
private byte[] _data;
private LargeStruct _control;
[GlobalSetup]
public void GlobalSetup()
{
_data = new byte[1000];
new Random().NextBytes(_data);
_control = new LargeStruct(_data[0], _data[1], _data[2], _data[3], _data[4], _data[5]);
}
[Benchmark]
public void PassSpanByValue()
{
for (int i = 0; i < Iterations; i++) AcceptSpanByValue(_data);
}
[Benchmark]
public void PassSpanByRef()
{
for (int i = 0; i < Iterations; i++) AcceptSpanByRef(_data);
}
[Benchmark]
public void PassLargeStructByValue()
{
for (int i = 0; i < Iterations; i++) AcceptLargeStructByValue(_control);
}
[Benchmark]
public void PassLargeStructByRef()
{
for (int i = 0; i < Iterations; i++) AcceptLargeStructByRef(_control);
}
private int AcceptSpanByValue(ReadOnlySpan<byte> span) => span.Length;
private int AcceptSpanByRef(in ReadOnlySpan<byte> span) => span.Length;
private decimal AcceptLargeStructByValue(LargeStruct largeStruct) => largeStruct.A;
private decimal AcceptLargeStructByRef(in LargeStruct largeStruct) => largeStruct.A;
private readonly struct LargeStruct
{
public LargeStruct(decimal a, decimal b, decimal c, decimal d, decimal e, decimal f)
{
A = a;
B = b;
C = c;
D = d;
E = e;
F = f;
}
public decimal A { get; }
public decimal B { get; }
public decimal C { get; }
public decimal D { get; }
public decimal E { get; }
public decimal F { get; }
}
}
我用这个重复了三次相同的基准测试,每次都得到了相似的结果:
BenchmarkDotNet=v0.10.13, OS=Windows 10 Redstone 3 [1709, Fall Creators Update] (10.0.16299.248)
Intel Core i7-4790 CPU 3.60GHz (Haswell), 1 CPU, 8 logical cores and 4 physical cores
Frequency=3507500 Hz, Resolution=285.1033 ns, Timer=TSC
.NET Core SDK=2.1.300-preview2-008354
[Host] : .NET Core 2.0.6 (CoreCLR 4.6.26212.01, CoreFX 4.6.26212.01), 64bit RyuJIT
DefaultJob : .NET Core 2.0.6 (CoreCLR 4.6.26212.01, CoreFX 4.6.26212.01), 64bit RyuJIT
Method | Mean | Error | StdDev |
----------------------- |----------:|----------:|----------:|
PassSpanByValue | 641.71 us | 0.1758 us | 0.1644 us |
PassSpanByRef | 642.62 us | 0.1524 us | 0.1190 us |
PassLargeStructByValue | 390.78 us | 0.2633 us | 0.2463 us |
PassLargeStructByRef | 35.33 us | 0.3446 us | 0.3055 us |
使用大型结构作为控件,我确认通过引用而不是通过值传递它们具有显着的性能优势。但是,通过引用或值传递 Span<T>
之间没有显着的性能差异。
2019 年 9 月更新
出于好奇,我 运行 再次使用 .NET Core 2.2 进行相同的基准测试。自上次以来似乎引入了一些巧妙的优化,以减少将数组隐式转换为 Span<T>
:
BenchmarkDotNet=v0.11.5, OS=Windows 10.0.17134.984 (1803/April2018Update/Redstone4)
Intel Core i7-4700HQ CPU 2.40GHz (Haswell), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=2.2.106
[Host] : .NET Core 2.2.4 (CoreCLR 4.6.27521.02, CoreFX 4.6.27521.01), 64bit RyuJIT
DefaultJob : .NET Core 2.2.4 (CoreCLR 4.6.27521.02, CoreFX 4.6.27521.01), 64bit RyuJIT
| Method | Mean | Error | StdDev |
|----------------------- |----------:|----------:|----------:|
| PassSpanByValue | 39.78 us | 0.1873 us | 0.1660 us |
| PassSpanByRef | 41.21 us | 0.2618 us | 0.2186 us |
| PassLargeStructByValue | 475.41 us | 1.3104 us | 1.0943 us |
| PassLargeStructByRef | 39.75 us | 0.1001 us | 0.0937 us |