[C#7.2]Span<t>

Sapnとは?

連続したメモリ領域への安全かつ効率的なアクセスを提供するための型

  • スタック上に必ず保持する(ヒープに置くような使い方はできない)

  • スライスで部分的にデータを取り出すことができる

  • ヒープにおけないので、GCの影響をうけない

実装例

1行コンマ区切りデータを受け取り分割する関数です

public static class SampleCode
{
    // Id(int), Name(string), Age(int), Date(DateTime)のCSV行を解析するメソッド
    public static (int Id, string Name, int Age, DateTime Date) ParseLine(string line)
    {
        // 文字列をReadOnlySpan<char>に変換
        ReadOnlySpan<char> span = line.AsSpan();

        // 1列目:Id
        int index1 = span.IndexOf(',');
        int id = int.Parse(span.Slice(0, index1));

        // 2列目:Name(sliceでコンマより後の文字列を参照)
        span = span.Slice(index1 + 1);
        int index2 = span.IndexOf(',');
        string name = span.Slice(0, index2).ToString();

        // 3列目:Age(sliceでコンマより後の文字列を参照)
        span = span.Slice(index2 + 1);
        int index3 = span.IndexOf(',');
        int age = int.Parse(span.Slice(0, index3));

        // 4列目:Date(sliceでコンマより後の文字列を参照)
        span = span.Slice(index3 + 1);
        DateTime date = DateTime.Parse(span);

        return (id, name, age, date);
    }
}
<code></pre>
ReadOnlySpan<T>は、スタックに積んだ値を参照するのみで、変更することができないことを意味します  
そのため、 span = span.Slice(index1 + 1); で参照する位置を変更していますが  
スタックに積んでいる値を変えているわけではないです  




### ベンチマーク

簡単な例で比較  

<pre class="line-numbers"><code class="language-csharp">using System;
using System.Globalization;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

[MemoryDiagnoser]
public class CsvParsingBenchmark
{
    private const string CsvLine = "12345,John Doe,42,2024-06-24";

    [Benchmark]
    public (int, string, int, DateTime) Parse_WithSplit()
    {
        var parts = CsvLine.Split(',');
        return (
            int.Parse(parts[0]),
            parts[1],
            int.Parse(parts[2]),
            DateTime.Parse(parts[3], CultureInfo.InvariantCulture)
        );
    }

    [Benchmark]
    public (int, string, int, DateTime) Parse_WithSpan()
    {
        ReadOnlySpan<char> span = CsvLine.AsSpan();

        int index1 = span.IndexOf(',');
        int id = int.Parse(span.Slice(0, index1));

        span = span.Slice(index1 + 1);
        int index2 = span.IndexOf(',');
        string name = span.Slice(0, index2).ToString();

        span = span.Slice(index2 + 1);
        int index3 = span.IndexOf(',');
        int age = int.Parse(span.Slice(0, index3));

        span = span.Slice(index3 + 1);
        DateTime date = DateTime.Parse(span, CultureInfo.InvariantCulture);

        return (id, name, age, date);
    }
}

public class Program
{
    public static void Main(string[] args)
    {
        var summary = BenchmarkRunner.Run<CsvParsingBenchmark>();
    }
}

※BenchmarkDotNetは、nugetで取得
※ベンチマークはリリースモードで実行

BenchmarkDotNet v0.15.2, Windows 11 (10.0.26100.4351/24H2/2024Update/HudsonValley)
13th Gen Intel Core i5-13400 2.50GHz, 1 CPU, 16 logical and 10 physical cores
.NET SDK 9.0.300
[Host] : .NET 8.0.16 (8.0.1625.21506), X64 RyuJIT AVX2 [AttachedDebugger]
DefaultJob : .NET 8.0.16 (8.0.1625.21506), X64 RyuJIT AVX2

Method Mean Error StdDev Gen0 Allocated
Parse_WithSplit 152.32 ns 3.029 ns 5.144 ns 0.0198 208 B
Parse_WithSpan 96.56 ns 1.955 ns 3.858 ns 0.0038 40 B

およそ、1.5倍の差ですね

スタック領域

各スレッドごとに固定サイズが割り当てられたメモリ領域
高速で動作するが、領域は大きくない

そのため、大量に確保するとスタックオーバーフローになるので要注意

そのため、データ量が多い場合には、ArrayPool(ヒープ領域に配置するが、再利用することで、無駄な確保を減らしGCを減らす)を使う

投稿日時: 2025-06-25 15:19:25
更新日時: 2025-06-25 16:18:25

内部リンク

Comment

最近の投稿

最近のコメント

タグ

アーカイブ

その他