Should ReadOnlySpan<T> parameters use the "in" modifier?

A key factor here is size; Span<T> / ReadOnlySpan<T> are deliberately very small, so the difference between a span and a reference-to-a-span is tiny. One key usage for in here is for larger readonly structs, to avoid a significant stack copy; note that there's a trade-off: the in is really a ref, so you're adding an extra layer of indirection to all access, unless the JIT sees what you're doing and works some voodoo. And of course: if the type doesn't declare itself as readonly then a stack copy is automatically added before the call to preserve the semantics.


Marc's answer seems spot-on. I'm posting this just to supplement his own answer with some benchmarks that confirm what he's saying.

I set up the following benchmark 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; }
    }
}

I repeated the same benchmark job three times with this and got similar results each time:

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 |

Using a large struct as a control, I confirm there are significant performance advantages when passing them by reference rather than by value. However, there are no significant performance differences between passing a Span<T> by reference or value.


September 2019 Update

Out of curiosity, I ran the same benchmarks again using .NET Core 2.2. There seem to have been some clever optimisations introduced since last time to reduce the overhead of implicitly casting an array to a 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 |