てくメモ

trivial な notes

【C#】Span を Stack<T>.PushRange する

前回記事の事実上の続き。
【C#】Span を List<T>.AddRange する - てくメモ


Stack<T>に複数の値を一度に渡すPushRange(span)的なものを考える。

事実上前回の続きと冒頭に書いてしまっているのだから、最初からそれを。
ビューという知見を活かすと次のように書ける。

public static void PushRange<T>(this Stack<T> stack, scoped ReadOnlySpan<T> items)
{
    int count = items.Length;
    if (count < 1) return;

    var len = stack.Count + count;
    stack.EnsureCapacity(len);

    ref var view = ref Unsafe.As<Stack<T>, ListView<T>>(ref stack);
    items.CopyTo(view._items.AsSpan()[stack.Count..]);
    view._size = len;
    view._version++;
}

// List<T> とフィールドの持ち方が一緒
internal sealed class ListView<T>
{
    public T[] _items;
    public int _size;
    public int _version;
}


やっていることがList<T>のときと実質同じなので、計測などは省略。
できるからといって基本は普通の手段でいいかな……というのも前回と同様。


なお、初期化時においてはIEnumerable<T>で渡せるなら、それを普通にコンストラクタに渡せば OK。
実体がICollection<T>なら、きちんと一度にコピーしてくれる。


ところで、PushRange の並びで TryPopRange はどうだろうと思ったが、以下のように試しに書いて、3個ずつポップして~ という処理をさせてみたところ、TryPopを3つ並べるより遅かった。

public static int TryPopRange<T>(this Stack<T> stack, scoped Span<T> resultBuffer)
{
    int len = resultBuffer.Length;
    if (stack.Count < len)
        len = stack.Count;
    if (len < 1) return 0;

    ref var view = ref Unsafe.As<Stack<T>, ListView<T>>(ref stack);
    var targetSpan = view._items.AsSpan().Slice(view._size - len, len);

    targetSpan.CopyTo(resultBuffer);
    resultBuffer.Reverse();
    if(RuntimeHelpers.IsReferenceOrContainsReferences<T>())
        targetSpan.Clear();
    view._size -= len;
    view._version++;

    return len;
}


まとまった数をプッシュするよりもともと恩恵が少ないというのと、あとはいちいちReverseをするのがあんまり……(試しにReverseを外したら逐次TryPopより微差程度に速かったが、もはやポップではない)

Unsafeであっても余計なことをすると、危険に加えてむしろ遅いという教訓。