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

usingステートメントは、C#1.0から使えるリソース管理の便利な仕組みですが
C#8.0からはusing 宣言として、ローカル変数のスコープを抜けたらDisposeする仕組みが追加されました

using ステートメントは、今日のリソース管理に効果的なツールですが、かなりの形式的な処理が必要です。 管理するリソースが多数あるメソッドでは、一連の using ステートメントでは構文的に行き詰まる可能性があります。 このような構文上の負担が大きいため、ほとんどのコーディング スタイル ガイドラインでは、このシナリオに対して中括弧に関する例外を明示的に規定しています。

using 宣言はここでの形式的な処理の多くを削除し、リソース管理ブロックを含む他の言語と同等の C# を取得します。 さらに、パターンベースの using を使用すると、開発者はここで参加できる型のセットを拡張できます。 多くの場合、using ステートメントで値を使用できるようにするためにのみ存在するラッパー型を作成する必要がなくなります。

これらの機能を組み合わせることで、開発者は using を適用できるシナリオを簡素化および拡張できます。 "パターン ベースの using" と "using 宣言"

using System;

public class Program
{
    public static void Main(string[] args)
    {


        // ブロックで囲われている間がスコープでブロックを出るとDisposeが呼ばれる
        using (var obj1 = new SampleCode())
        {
            Console.WriteLine("usingステートメントは、C#1.0から使える");
        }

        if (true)
        {
            // using宣言は、ローカル変数のスコープがusingのスコープになる(if文抜けたらDisposeする)
            using var obj2 = new SampleCode();

            Console.WriteLine("using宣言は、C#8.0から使える");
        }

        Console.WriteLine("範囲のわかりやすさでは、usingステートメント");
        Console.WriteLine("ネストを嫌うのであれば、using宣言が良いかも?");
    }
}

public class SampleCode : IDisposable
{
    public void Dispose()
    {
        Console.WriteLine("Dispose!!");
    }
}
投稿日時: 2025-06-24 13:10:24

従来の書き方だと以下のように書く内容を・・・

public class User
{
    private string FirstName { get; set; }
    private string LastName { get; set; }

    public User(string firstName, string lastName)
    {
        this.FirstName = firstName;
        this.LastName = lastName;
    }
}

コンストラクタの引数の情報をクラスのところに移動し
そこに記載している変数を初期化に設定に使えることで短くかけるという話

public class User(string firstName, string lastName)
{
    private string FirstName { get; set; } = firstName;
    private string LastName { get; set; } = lastName;
}

なかなか独特な感じがしますね・・。
複数のコンストラクターがある場合はNGです

投稿日時: 2025-06-24 12:30:24

最近の投稿

最近のコメント

タグ

アーカイブ

その他