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 |