C# - Span<T>와 Memory<T>
Span<T>에 대해서는 전에,
C# 7.2 - Span<T>
; https://www.sysnet.pe.kr/2/0/11534
C# - System.Span<T> 성능
; https://www.sysnet.pe.kr/2/0/11535
소개한 적이 있으니, 이번엔 Memory<T>를
Memory<T> Struct
; https://learn.microsoft.com/en-us/dotnet/api/system.memory-1
추가해 설명하겠습니다. 우선 성능을 볼 텐데, (최소 지원 버전인) .NET Framework 4.5 + Nuget System.Memory 4.5.2로 구성해,
using System;
using System.Diagnostics;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
Action<int, string, Action<byte[]>, byte[]> action = (loopCount, title, work, arg) =>
{
Stopwatch st = new Stopwatch();
st.Start();
Random rand = new Random(Environment.TickCount);
for (int i = 0; i < loopCount; i++)
{
work(arg);
}
st.Stop();
Console.WriteLine(title + " : " + st.ElapsedMilliseconds);
};
byte[] buf = new byte[1];
action(1, "touch-JIT", ForLoop, buf);
action(1, "touch-JIT", MemoryLoop, buf);
action(1, "touch-JIT", PtrLoop, buf);
Console.WriteLine();
buf = new byte[10000];
action(100000, "ForLoop", ForLoop, buf);
action(100000, "MemoryLoop", MemoryLoop, buf);
action(100000, "PtrLoop", PtrLoop, buf);
}
static void ForLoop(byte[] buffer)
{
int sum = 0;
for (int i = 0; i < buffer.Length; i++)
{
sum += buffer[i];
}
}
static void MemoryLoop(byte[] buffer)
{
Memory<byte> memory = buffer;
int sum = 0;
for (int i = 0; i < memory.Length; i++)
{
sum += memory.Span[i];
}
}
static unsafe void PtrLoop(byte[] buffer)
{
int sum = 0;
fixed (byte* ptr = buffer)
{
for (int i = 0; i < buffer.Length; i++)
{
sum += *(ptr + i);
}
}
}
}
}
실행하면 이런 결과를 얻습니다.
// .NET 4.5 + Release
ForLoop : 708
MemoryLoop : 6822
PtrLoop : 569
// .NET Core 2.1 + Release
ForLoop : 597
MemoryLoop : 6044
PtrLoop : 466
보다시피 Memory<T>의 성능은 일반적인 배열과 비교해 약 10배 정도 느립니다.
하지만, 그렇다고 해서 Memory<T>에 대해 크게 실망할 필요는 없습니다. 왜냐하면, 사실 Memory<T>.Span 속성은 Span<T> 타입인데 이를 가볍게 캐시만 해서 사용하는 코드로 바꾸면,
static int MemorySpanLoop(byte[] buffer)
{
Memory<byte> memory = buffer;
Span<byte> span = memory.Span;
int sum = 0;
for (int i = 0; i < span.Length; i++)
{
sum += span[i];
}
return sum;
}
이번엔 다음과 같은 결과를 확인할 수 있습니다.
// .NET 4.5 + Release
ForLoop : 623
MemoryLoop : 6095
MemorySpanLoop : 907
PtrLoop : 434
// .NET Core 2.1 + Release
ForLoop : 540
MemoryLoop : 10770
MemorySpanLoop : 440
PtrLoop : 428
거의
Span<T>와 다름없는 속도입니다.
(결과에서 유추해 보면, 관리 포인터로 인한 혜택은 (907 - 440) 정도의 속도 차이만 나고, 그 외의 성능 손실은 Memory<T>.Span 속성이
단순히 내부의 변수 하나를 반환하는 것이 아닌, 복잡한 코드를 포함하고 있기 때문에 그것 자체의 메서드 처리가 문제였을 것입니다.)
그나저나, Memory<T> 타입과 Span<T> 타입의 차이점이 뭘까요? "
C# 7.2 - Span<T>" 글에서 Span은 "ref struct"이기 때문에 스택에만 생성할 수 있다고 했습니다. 즉, 다른 타입의 필드로 Span<T>를 정의할 수 없습니다. 반면, Memory<T>는 그냥 struct이기 때문에 관리 힙에도 위치할 수 있으므로 Span<T>와 같은 제약이 없습니다.
class MyType
{
// 컴파일 오류
// Error CS8345 Field or auto-implemented property cannot be of type 'Span<byte>' unless it is an instance member of a ref struct
public Span<byte> ByteBuffer;
// 사용 가능
public Memory<byte> MemoryBuffer;
}
따라서, 사용 원칙은 간단합니다. 1) 평소에는 성능을 위해 Span<T>를 사용하고, 2) 간혹 해당 버퍼를 다른 타입의 필드로 들고 있어야 할 때 Memory<T>를 사용하다가, 3) 다시 그것을 접근해야 할 때는 Span<T>로 캐시해 사용하는 것입니다.
{
byte[] buffer = new byte[1000];
MyType type = new MyType();
type.MemoryBuffer = buffer; // 필드에 들고 있어야 할 때는 Memory<T>로.
// 그 필드를 다시 사용해야 할 때는 Span<T>로.
Span<byte> fastBuf = type.MemoryBuffer.Span;
for (int i = 0; i < fastBuf.Length; i ++)
{
// ...
}
}
(
첨부 파일은 이 글의 예제 코드를 포함합니다.)
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]