Span<T> のコピー
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
を呼び出しています。count
とoffset
のパラメータはここではバイトです。伝統的な配列のインデックスやカウントを指定したい場合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;
}
}
}
}
実装的にはほぼ同一です。
ベンチマークをしてみました。
.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
した配列への書き込みが遅いのはなんでだろう。。コピーが走っていると思って dest
を ref
にしてみましたが変わらず。引き回す Span
は stackalloc
しないほうが良いのかも?
Discussion