Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

C# 7.2 introduced reference semantics with value-types, and alongside this Microsoft have developed types like Span<T> and ReadOnlySpan<T> to potentially improve performance for apps that need to perform operations on contiguous regions of memory.

According to the docs, one way of potentially improving performance is to pass immutable structs by reference by adding an in modifier to parameters of those types:

void PerformOperation(in SomeReadOnlyStruct value)
{
}

What I'm wondering is whether I ought to do this with types like ReadOnlySpan<T>. Should I be declaring methods that accept a read-only span like this:

void PerformOperation<T>(in ReadOnlySpan<T> value)
{
}

or simply like:

void PerformOperation<T>(ReadOnlySpan<T> value)
{
}

Will the former offer any performance benefits over the latter? I couldn't find any documentation that explicitly advises in either direction, but I did find this example where they demonstrated a method that accepts a ReadOnlySpan and did not use the in modifier.

like image 598
Tagc Avatar asked Mar 15 '18 11:03

Tagc


2 Answers

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 |
like image 53
Tagc Avatar answered Oct 08 '22 05:10

Tagc


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.

like image 23
Marc Gravell Avatar answered Oct 08 '22 05:10

Marc Gravell