はじめに
.net9-preview7 が公開されました。新機能のひとつに ReadOnlySpan<char>.Split()
オーバーロードがあるようです。
Enumerate over ReadOnlySpan.Split() segments
var span = "Hello, World!".AsSpan();
var index = 0;
foreach (var n in span.Split(','))
{
switch (index)
{
case 0:
Assert.Equal("Hello", span[n]);
break;
case 1:
Assert.Equal(" World!", span[n]);
break;
}
++index;
}
新機能の利点
従来の Sprit()
と比較して何が違うのかというと、事前にバッファを確保する必要がなくなった点です。
// 従来の方法
var span = "Hello, World!".AsSpan();
var buffer = (stackalloc Range[2]); // バッファの確保が必要
_ = span.Split(buffer, ',');
Assert.Equal("Hello", span[buffer[0]]);
Assert.Equal(" World!", span[buffer[1]]);
また列挙パターンになったため、列挙の途中で処理を中断することもできます。これは非常に長いテキストをパースするときに役立つかもしれません。
var span = longLongText.AsSpan();
foreach (var n in span.Split(','))
{
if (span[n].SequenceEqual("特定のワード"))
// 処理を中断
break;
}
パフォーマンス比較
テストコード
using Xunit;
public class _ReadOnlySpanSplitTest
{
[Fact]
static void HowToUse()
{
var span = "Hello, World!".AsSpan();
var index = 0;
foreach (var n in span.Split(','))
{
switch (index)
{
case 0:
Assert.Equal("Hello", span[n]);
break;
case 1:
Assert.Equal(" World!", span[n]);
break;
}
++index;
}
}
[Fact]
static void OlderSplitPattern()
{
var span = "Hello, World!".AsSpan();
var buffer = (stackalloc Range[2]);
_ = span.Split(buffer, ',');
Assert.Equal("Hello", span[buffer[0]]);
Assert.Equal(" World!", span[buffer[1]]);
}
static void Example(string longLongText)
{
var span = longLongText.AsSpan();
foreach (var n in span.Split(','))
{
if (span[n].SequenceEqual("特定のワード"))
// 処理を中断
break;
}
}
public static bool TrySplitIndex<T>(ReadOnlySpan<T> span, ReadOnlySpan<T> separator, out ReadOnlySpan<T> split, scoped ref int index) where T : IEquatable<T>
{
if (index < 0)
{
split = default;
return false;
}
var span2 = span.Slice(index);
var index2 = span2.IndexOfAny(separator);
if (index2 < 0)
{
split = span2;
index = -1;
}
else
{
split = span2.Slice(0, index2);
index += index2 + separator.Length;
}
return true;
}
[Fact]
static void TrySplitIndexTest()
{
var span = "Hello, World!".AsSpan();
var index = 0;
ReadOnlySpan<char> split;
Assert.True(TrySplitIndex(span, ",".AsSpan(), out split, ref index));
Assert.Equal("Hello", split);
Assert.True(TrySplitIndex(span, ",".AsSpan(), out split, ref index));
Assert.Equal(" World!", split);
Assert.False(TrySplitIndex(span, ",".AsSpan(), out _, ref index));
}
static void TrySplitIndexHowToUse()
{
var span = "Hello, World!".AsSpan();
var index = 0;
while (TrySplitIndex(span, ",".AsSpan(), out var split, ref index))
{
// 処理
}
}
static void Performance(Performance p)
{
const string text = "a,bb,ccc,dddd,eeeee,ffffff,ggggggg,hhhhhhhhh,iiiiiiiiii,jjjjjjjjjjj";
p.AddTest("SpanSplitEnumerator", () =>
{
var span = text.AsSpan();
for (int i = 0; i < 10000; ++i)
{
var sumLength = 0;
foreach (var n in span.Split(','))
sumLength += span[n].Length;
}
});
p.AddTest("SplitUseBuffer", () =>
{
var span = text.AsSpan();
var buffer = (stackalloc Range[10]);
for (int i = 0; i < 10000; ++i)
{
var sumLength = 0;
var count = span.Split(buffer, ',');
var splits = buffer.Slice(0, count);
foreach (var n in splits)
sumLength += span[n].Length;
}
});
p.AddTest("TrySplitIndex", () =>
{
var span = text.AsSpan();
for (int i = 0; i < 10000; ++i)
{
var sumLength = 0;
var index = 0;
while (TrySplitIndex(span, ",", out var split, ref index))
sumLength += split.Length;
}
});
p.AddTest("string.Split", () =>
{
for (int i = 0; i < 10000; ++i)
{
var sumLength = 0;
foreach (var n in text.Split(','))
sumLength += n.Length;
}
});
}
}
パフォーマンス的には従来の方法と同程度です。string.Split()
よりパフォーマンスが良いです。
Test | Score | % | CG0 |
---|---|---|---|
SpanSplitEnumerator | 177 | 100.0% | 0 |
SplitUseBuffer | 181 | 102.3% | 0 |
TrySplitIndex | 249 | 140.7% | 0 |
string.Split | 62 | 35.0% | 34 |
実行環境: Windows11 x64 .NET Runtime 9.0.0
Score は高いほどパフォーマンスがよいです。
GC0 はガベージコレクション回数を表します(少ないほどパフォーマンスがよい)。
-
string.Split
はヒープが発生しパフォーマンスもあまり良くないです。可能なら新しい方法を採用したいところです。 -
TrySplitIndex
は独自実装のメソッドです。なぜか最もパフォーマンスが良いです。
おまけ TrySplitIndex
Span<T>
の出始めの頃に自前で実装した関数です。列挙パターンではありませんが、ヒープが発生しないようになっています(詳細はテストコードを参照)。
var span = "Hello, World!".AsSpan();
var index = 0;
while (TrySplitIndex(span, ",".AsSpan(), out var split, ref index))
{
// 処理
}