てくメモ

trivial な notes

【C#】ReadOnlySpan<T>最適化の確認

以前、最適化によりbyte[], sbyte[]がゼロアロケーションとなる場合を確認した。
【C#】ゼロアロケーションバイト列 - てくメモ


これについて、現在1は対象が拡大している。

参考
コレクション式 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C


また、C# 12 (.NET 8) ではコレクション式の導入により、new T[]と書いてアロケーションがないという見た目だったのが解消された。

// これらはアロケーション回避
private ReadOnlySpan<byte> ByteArray => [1, 2, 3];
private ReadOnlySpan<sbyte> SbyteArray => [1, 2, 3];
private ReadOnlySpan<int> IntArray => [1, 2, 3];
private ReadOnlySpan<uint> UintArray => [1, 2, 3];
private ReadOnlySpan<float> SingleArray => [1, 2, 3];
private ReadOnlySpan<double> DoubleArray => [1, 2, 3];
private ReadOnlySpan<short> ShortArray => [1, 2, 3];
private ReadOnlySpan<ushort> UshortArray => [1, 2, 3];
private ReadOnlySpan<long> LongArray => [1, 2, 3];
private ReadOnlySpan<ulong> UlongArray => [1, 2, 3];
private ReadOnlySpan<char> CharArray => ['a', 'b', 'c'];
private ReadOnlySpan<bool> BoolArray => [true, false, true];

// これらはダメのよう
// private ReadOnlySpan<decimal> DecimalArray => [1, 2, 3];
// private ReadOnlySpan<nint> NintArray => [1, 2, 3];
// private ReadOnlySpan<nuint> NuintArray => [1, 2, 3];

以下に参考で IL を折りたたむ。

折りたたみ(確認用・長い) ※ ZeroAllocArraySandbox はメンバーを置いたクラス名

.method private hidebysig specialname 
    instance valuetype [System.Runtime]System.ReadOnlySpan`1<uint8> get_ByteArray () cil managed 
{
    // Method begins at RVA 0x2224
    // Header size: 1
    // Code size: 12 (0xc)
    .maxstack 8

 IL_0000: ldsflda valuetype '<PrivateImplementationDetails>'/'__StaticArrayInitTypeSize=3' '<PrivateImplementationDetails>'::'039058C6F2C0CB492C533B0A4D14EF77CC0F78ABCCCED5287D84A1A2011CFB81'
 IL_0005: ldc.i4.3
 IL_0006: newobj instance void valuetype [System.Runtime]System.ReadOnlySpan`1<uint8>::.ctor(void*, int32)
 IL_000b: ret
} // end of method ZeroAllocArraySandbox::get_ByteArray

.method private hidebysig specialname 
    instance valuetype [System.Runtime]System.ReadOnlySpan`1<int8> get_SbyteArray () cil managed 
{
    // Method begins at RVA 0x2231
    // Header size: 1
    // Code size: 12 (0xc)
    .maxstack 8

 IL_0000: ldsflda valuetype '<PrivateImplementationDetails>'/'__StaticArrayInitTypeSize=3' '<PrivateImplementationDetails>'::'039058C6F2C0CB492C533B0A4D14EF77CC0F78ABCCCED5287D84A1A2011CFB81'
 IL_0005: ldc.i4.3
 IL_0006: newobj instance void valuetype [System.Runtime]System.ReadOnlySpan`1<int8>::.ctor(void*, int32)
 IL_000b: ret
} // end of method ZeroAllocArraySandbox::get_SbyteArray

.method private hidebysig specialname 
    instance valuetype [System.Runtime]System.ReadOnlySpan`1<int32> get_IntArray () cil managed 
{
    // Method begins at RVA 0x223e
    // Header size: 1
    // Code size: 11 (0xb)
    .maxstack 8

 IL_0000: ldtoken field valuetype '<PrivateImplementationDetails>'/'__StaticArrayInitTypeSize=12_Align=4' '<PrivateImplementationDetails>'::'4636993D3E1DA4E9D6B8F87B79E8F7C6D018580D52661950EABC3845C5897A4D4'
 IL_0005: call valuetype [System.Runtime]System.ReadOnlySpan`1<!!0> [System.Runtime]System.Runtime.CompilerServices.RuntimeHelpers::CreateSpan<int32>(valuetype [System.Runtime]System.RuntimeFieldHandle)
 IL_000a: ret
} // end of method ZeroAllocArraySandbox::get_IntArray

.method private hidebysig specialname 
    instance valuetype [System.Runtime]System.ReadOnlySpan`1<uint32> get_UintArray () cil managed 
{
    // Method begins at RVA 0x224a
    // Header size: 1
    // Code size: 11 (0xb)
    .maxstack 8

 IL_0000: ldtoken field valuetype '<PrivateImplementationDetails>'/'__StaticArrayInitTypeSize=12_Align=4' '<PrivateImplementationDetails>'::'4636993D3E1DA4E9D6B8F87B79E8F7C6D018580D52661950EABC3845C5897A4D4'
 IL_0005: call valuetype [System.Runtime]System.ReadOnlySpan`1<!!0> [System.Runtime]System.Runtime.CompilerServices.RuntimeHelpers::CreateSpan<uint32>(valuetype [System.Runtime]System.RuntimeFieldHandle)
 IL_000a: ret
} // end of method ZeroAllocArraySandbox::get_UintArray

.method private hidebysig specialname 
    instance valuetype [System.Runtime]System.ReadOnlySpan`1<float32> get_SingleArray () cil managed 
{
    // Method begins at RVA 0x2256
    // Header size: 1
    // Code size: 11 (0xb)
    .maxstack 8

 IL_0000: ldtoken field valuetype '<PrivateImplementationDetails>'/'__StaticArrayInitTypeSize=12_Align=4' '<PrivateImplementationDetails>'::'8E628779E6A74EE0B36991C10158F63CAFEC7D340AD4E075592502C8708524DD4'
 IL_0005: call valuetype [System.Runtime]System.ReadOnlySpan`1<!!0> [System.Runtime]System.Runtime.CompilerServices.RuntimeHelpers::CreateSpan<float32>(valuetype [System.Runtime]System.RuntimeFieldHandle)
 IL_000a: ret
} // end of method ZeroAllocArraySandbox::get_SingleArray

.method private hidebysig specialname 
    instance valuetype [System.Runtime]System.ReadOnlySpan`1<float64> get_DoubleArray () cil managed 
{
    // Method begins at RVA 0x2262
    // Header size: 1
    // Code size: 11 (0xb)
    .maxstack 8

 IL_0000: ldtoken field valuetype '<PrivateImplementationDetails>'/'__StaticArrayInitTypeSize=24_Align=8' '<PrivateImplementationDetails>'::A68DE4B5E96A60C8CEB3C7B7EF93461725BDBBFF3516B136585A743B5C0EC6648
 IL_0005: call valuetype [System.Runtime]System.ReadOnlySpan`1<!!0> [System.Runtime]System.Runtime.CompilerServices.RuntimeHelpers::CreateSpan<float64>(valuetype [System.Runtime]System.RuntimeFieldHandle)
 IL_000a: ret
} // end of method ZeroAllocArraySandbox::get_DoubleArray

.method private hidebysig specialname 
    instance valuetype [System.Runtime]System.ReadOnlySpan`1<int16> get_ShortArray () cil managed 
{
    // Method begins at RVA 0x226e
    // Header size: 1
    // Code size: 11 (0xb)
    .maxstack 8

 IL_0000: ldtoken field valuetype '<PrivateImplementationDetails>'/'__StaticArrayInitTypeSize=6_Align=2' '<PrivateImplementationDetails>'::'047DBF5366372631BA7E3E02520E651446B899C96C4B64663BAC378A298A7BF72'
 IL_0005: call valuetype [System.Runtime]System.ReadOnlySpan`1<!!0> [System.Runtime]System.Runtime.CompilerServices.RuntimeHelpers::CreateSpan<int16>(valuetype [System.Runtime]System.RuntimeFieldHandle)
 IL_000a: ret
} // end of method ZeroAllocArraySandbox::get_ShortArray

.method private hidebysig specialname 
    instance valuetype [System.Runtime]System.ReadOnlySpan`1<uint16> get_UshortArray () cil managed 
{
    // Method begins at RVA 0x227a
    // Header size: 1
    // Code size: 11 (0xb)
    .maxstack 8

 IL_0000: ldtoken field valuetype '<PrivateImplementationDetails>'/'__StaticArrayInitTypeSize=6_Align=2' '<PrivateImplementationDetails>'::'047DBF5366372631BA7E3E02520E651446B899C96C4B64663BAC378A298A7BF72'
 IL_0005: call valuetype [System.Runtime]System.ReadOnlySpan`1<!!0> [System.Runtime]System.Runtime.CompilerServices.RuntimeHelpers::CreateSpan<uint16>(valuetype [System.Runtime]System.RuntimeFieldHandle)
 IL_000a: ret
} // end of method ZeroAllocArraySandbox::get_UshortArray

.method private hidebysig specialname 
    instance valuetype [System.Runtime]System.ReadOnlySpan`1<int64> get_LongArray () cil managed 
{
    // Method begins at RVA 0x2286
    // Header size: 1
    // Code size: 11 (0xb)
    .maxstack 8

 IL_0000: ldtoken field valuetype '<PrivateImplementationDetails>'/'__StaticArrayInitTypeSize=24_Align=8' '<PrivateImplementationDetails>'::E2E2033AE7E19D680599D4EB0A1359A2B48EC5BAAC75066C317FBF85159C54EF8
 IL_0005: call valuetype [System.Runtime]System.ReadOnlySpan`1<!!0> [System.Runtime]System.Runtime.CompilerServices.RuntimeHelpers::CreateSpan<int64>(valuetype [System.Runtime]System.RuntimeFieldHandle)
 IL_000a: ret
} // end of method ZeroAllocArraySandbox::get_LongArray

.method private hidebysig specialname 
    instance valuetype [System.Runtime]System.ReadOnlySpan`1<uint64> get_UlongArray () cil managed 
{
    // Method begins at RVA 0x2292
    // Header size: 1
    // Code size: 11 (0xb)
    .maxstack 8

 IL_0000: ldtoken field valuetype '<PrivateImplementationDetails>'/'__StaticArrayInitTypeSize=24_Align=8' '<PrivateImplementationDetails>'::E2E2033AE7E19D680599D4EB0A1359A2B48EC5BAAC75066C317FBF85159C54EF8
 IL_0005: call valuetype [System.Runtime]System.ReadOnlySpan`1<!!0> [System.Runtime]System.Runtime.CompilerServices.RuntimeHelpers::CreateSpan<uint64>(valuetype [System.Runtime]System.RuntimeFieldHandle)
 IL_000a: ret
} // end of method ZeroAllocArraySandbox::get_UlongArray

.method private hidebysig specialname 
    instance valuetype [System.Runtime]System.ReadOnlySpan`1<char> get_CharArray () cil managed 
{
    // Method begins at RVA 0x229e
    // Header size: 1
    // Code size: 11 (0xb)
    .maxstack 8

 IL_0000: ldtoken field valuetype '<PrivateImplementationDetails>'/'__StaticArrayInitTypeSize=6_Align=2' '<PrivateImplementationDetails>'::'13E228567E8249FCE53337F25D7970DE3BD68AB2653424C7B8F9FD05E33CAEDF2'
 IL_0005: call valuetype [System.Runtime]System.ReadOnlySpan`1<!!0> [System.Runtime]System.Runtime.CompilerServices.RuntimeHelpers::CreateSpan<char>(valuetype [System.Runtime]System.RuntimeFieldHandle)
 IL_000a: ret
} // end of method ZeroAllocArraySandbox::get_CharArray

.method private hidebysig specialname 
    instance valuetype [System.Runtime]System.ReadOnlySpan`1<bool> get_BoolArray () cil managed 
{
    // Method begins at RVA 0x22aa
    // Header size: 1
    // Code size: 12 (0xc)
    .maxstack 8

 IL_0000: ldsflda valuetype '<PrivateImplementationDetails>'/'__StaticArrayInitTypeSize=3' '<PrivateImplementationDetails>'::'85F90DFEA1D8027E1463E5CA971A250110A20DF0119D204A74220BC63516D15B'
 IL_0005: ldc.i4.3
 IL_0006: newobj instance void valuetype [System.Runtime]System.ReadOnlySpan`1<bool>::.ctor(void*, int32)
 IL_000b: ret
} // end of method ZeroAllocArraySandbox::get_BoolArray


最後に、配列の書き方と並べて BenchmarkDotNet にてアロケーションを見てみる。

[Benchmark]
public int Alloc()
{
    bool[] bs = [false, false, true];
    static int challenge(ReadOnlySpan<bool> span, int count)
    {
        var result = 0;
        for (int i = 0; i < count; i++)
        {
            if (span[i % span.Length]) result++;
        }
        return result;
    }

    return challenge(bs, 10);
}

[Benchmark]
public double ZeroAlloc()
{
    ReadOnlySpan<bool> bs = [false, false, true];
    static int challenge(ReadOnlySpan<bool> span, int count)
    {
        var result = 0;
        for (int i = 0; i < count; i++)
        {
            if (span[i % span.Length]) result++;
        }
        return result;
    }

    return challenge(bs, 10);
}
 Method    | Mean     | Error    | StdDev   | Gen0   | Allocated |
---------- |---------:|---------:|---------:|-------:|----------:|
 Alloc     | 19.33 ns | 2.002 ns | 0.110 ns | 0.0102 |      32 B |
 ZeroAlloc | 17.86 ns | 7.375 ns | 0.404 ns |      - |         - |

確かにアロケーションなし。




  1. C# 12 / .NET 8 からだと思っていたら、今回 C# 11 / .NET 7 でコンパイルしたものを違いを見るために確認したら C# 12 と同様の IL だったので(C# 10 / .NET 6 だと違った)、とりあえず現在と表現。