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.
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 |
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With