4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【.net9-preview7】列挙可能な ReadOnlySpan<char>.Split() が追加されました

Posted at

はじめに

.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))
{
    // 処理
}
4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?