📖

Span<T> のコピー

2022/10/18に公開

Qiitaより転載
2019/9/28 初投稿


追記 (2019/10/5)

Slice して CopyTo したらええやん?とのことでした。たし🦀
しかもバグってるっていうね。どんまい。
この記事は記録のため残しておきます。


配列をコピーする場合、.NET には以下の API が用意されている。

  • Array.Copy(Array, int, Array, int, int)
  • Buffer.BlockCopy(Array, int, Array, int, int)

引数が示す通り、上記は Array にしか対応していないが、昨今の C# 界隈では Span を使った配列操作が目玉になっているので、Span に対応したコピーメソッドを作ることにした。

手始めに、Buffer.BlockCopy のソースコードを見たところ、以下のようにコメントされていた。

        // Copies from one primitive array to another primitive array without
        // respecting types.  This calls memmove internally.  The count and 
        // offset parameters here are in bytes.  If you want to use traditional
        // array element indices and counts, use Array.Copy.
        [System.Security.SecuritySafeCritical]  // auto-generated
        [ResourceExposure(ResourceScope.None)]
        [MethodImplAttribute(MethodImplOptions.InternalCall)]
        public static extern void BlockCopy(Array src, int srcOffset,
            Array dst, int dstOffset, int count);

意訳

一つのプリミティブな配列から別のプリミティブな配列に型を無視してコピーします。内部的には memmove を呼び出しています。countoffset のパラメータはここではバイトです。伝統的な配列のインデックスやカウントを指定したい場合 Array.Copy を使用してください。

ということで、memmove を検索してみると gcc のソースコードが出てきました。

/* Public domain.  */
#include <stddef.h>

void *
memmove (void *dest, const void *src, size_t len)
{
  char *d = dest;
  const char *s = src;
  if (d < s)
    while (len--)
      *d++ = *s++;
  else
    {
      char *lasts = s + (len-1);
      char *lastd = d + (len-1);
      while (len--)
        *lastd-- = *lasts--;
    }
  return dest;
}

見た感じただ配列の要素をコピーしてるだけっぽいので、C# で実装してみました。

public static class Span
{
    public static void Copy<T>(ReadOnlySpan<T> src, int sStart, Span<T> dest, int dStart, int len)
    {
        if (dest.Length < src.Length)
        {
            int i = 0;
            while (len-- > 0 && dStart + i < dest.Length)
            {
                dest[dStart + i] =  src[sStart + i];
                i += 1;
            }
        }
        else
        {
            int lastS = sStart + (len - 1);
            int lastD = dStart + (len - 1);
            int i = 0;
            while (len-- > 0)
            {
                dest[lastD - i] =  src[lastS - i];
                i += 1;
            }
        }
    }
}

実装的にはほぼ同一です。
ベンチマークをしてみました。

https://gist.github.com/akiraKido/6c2fcaed20ff86663025815ca782a8f9

.NET Core 2.1.12

Method Mean Error StdDev
ArrayCopy 22.71 ns 0.1647 ns 0.1375 ns
BufferBlockCopy 23.02 ns 0.5288 ns 1.5591 ns
SpanCopy 28.11 ns 0.7507 ns 1.0766 ns
SpanCopyStackAlloc 24.64 ns 0.5107 ns 0.5015 ns

.NET Core 3.0.100

Method Mean Error StdDev
ArrayCopy 22.38 ns 0.4569 ns 0.5940 ns
BufferBlockCopy 20.06 ns 0.1306 ns 0.1222 ns
SpanCopy 19.95 ns 0.3644 ns 0.3043 ns
SpanCopyStackAlloc 23.81 ns 0.1139 ns 0.1065 ns

.NET Core 2.1.12 だと他と比べるとだいぶ遅いですが、.NET Core 3.0.100 だと一番早くなってます。Span 周りに最適化があったのかな? しかし .NET Core 3.0.100 で stackalloc した配列への書き込みが遅いのはなんでだろう。。コピーが走っていると思って destref にしてみましたが変わらず。引き回す Spanstackalloc しないほうが良いのかも?

Discussion