はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【C#】Span<T>とString.Substringでどれくらい速度差が出てくるのか調べてみる

はじめに

今回はSpan<T>String.Substringでどれくらい速度差が出てくるのか調べてみる記事になります。
https://docs.microsoft.com/ja-jp/dotnet/api/system.span-1?view=net-6.0
String.Substring メソッド (System) | Microsoft Docs

一応公式ドキュメントにSpan<T>Sliceという項目があり、以下のように書かれています。

Spanには、現在のスパンより、指定したインデックスから始まるスライスを形成するSliceメソッドの 2 つのオーバー ロードが含まれています。 これにより、Spanのデータを、パフォーマンスの影響を最小限にしながら、データ処理パイプラインの一部で必要に応じて処理できる、一連の論理的なまとまりとして扱うことができます。 たとえば、最新のサーバー プロトコルは多くの場合、テキスト ベースであるため、文字列と部分文字列の操作は特に重要です。 Stringクラスの場合、部分文字列の抽出に使う主要なメソッドはSubstringです。 広範な文字列操作に依存するデータ パイプラインは、次のように、いくつかのパフォーマンスの低下を招きます。
1. 部分文字列を保持する新しい文字列を作成します。
2. 元の文字列から新しい文字列に文字のサブセットをコピーします。

https://docs.microsoft.com/ja-jp/dotnet/api/system.span-1?view=net-6.0#spant-and-slices


まあそうなのでしょうが、一応疑り深いので手元でも試してみました。

環境

.NET Core3.1
Visual Studio v17.0.5
MacOS

実験

// 適当に長そうな文字列を作る
string sample = Enumerable.Range(0, 100)
    .Select(_ => Guid.NewGuid().ToString())
    .Aggregate((x, y) => x + y);

// 3600文字
Console.WriteLine(sample);
Console.WriteLine($"Length : {sample.Length}");

const int count = 10000000;
const int startIndex = 1000;
const int length = 1000;
var sw = new Stopwatch();

// String.Substring
sw.Start();
for (var i = 0; i < count; i++)
{
    string _ = sample.Substring(startIndex, length);
}
sw.Stop();
Console.WriteLine($"string.Substring : {sw.ElapsedMilliseconds}ms");

// Span<T>
sw.Reset();
sw.Start();
for (var i = 0; i < count; i++)
{
    ReadOnlySpan<char> _ = sample.AsSpan().Slice(startIndex, length);
}
sw.Stop();
Console.WriteLine($"Span<T> : {sw.ElapsedMilliseconds}ms");

結果

種類 処理時間
string.Substring 1395ms
Span<T> 153ms

上記のコードを実行してみたところ、結構速度に違いが出てきているようです。

補足

実験で用いたコードの返り値が異なることに注意してください。

string _ = sample.Substring(startIndex, length);
ReadOnlySpan<char> _ = sample.AsSpan().Slice(startIndex, length);

具体的に何が違うのかといったものは未確認飛行さんの部分参照の箇所を参照すると良いと思います。
Span<T>構造体 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C

補足2

また少し面白い実験としてReadOnlySpanstringに変換したら、割と同じ処理時間になります。(少しオーバーヘッドがあるが)

ReadOnlySpan<char> a = sample.AsSpan().Slice(startIndex, length);
_ = new string(a);
種類 処理時間
string.Substring 1340ms
Span<T> 1554ms