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