Microsoft MVP성태의 닷넷 이야기
.NET Framework: 995. C# - Span<T>와 Memory<T> [링크 복사], [링크+제목 복사],
조회: 27658
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 3개 있습니다.)
(시리즈 글이 5개 있습니다.)
.NET Framework: 759. C# - System.Span<T> 성능
; https://www.sysnet.pe.kr/2/0/11535

.NET Framework: 768. BenchmarkDotNet으로 Span<T> 성능 측정
; https://www.sysnet.pe.kr/2/0/11550

.NET Framework: 995. C# - Span<T>와 Memory<T>
; https://www.sysnet.pe.kr/2/0/12475

.NET Framework: 1002. C# - ReadOnlySequence<T> 소개
; https://www.sysnet.pe.kr/2/0/12484

.NET Framework: 1112. C# - .NET 6부터 공개된 ISpanFormattable 사용법
; https://www.sysnet.pe.kr/2/0/12821




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 ++)
    {
        // ... 
    }
}

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 2/17/2024]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2021-01-05 08시56분
정성태

[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
14025정성태10/14/2025658Linux: 126. eBPF (bpf2go) - tcp_sendmsg 예제
14024정성태10/14/2025689오류 유형: 984. Whisper.net - System.Exception: 'Cannot dispose while processing, please use DisposeAsync instead.'
14023정성태10/12/20251166닷넷: 2369. C# / Whisper 모델 - 동영상의 음성을 인식해 자동으로 SRT 자막 파일을 생성 [1]파일 다운로드1
14022정성태10/10/20252022닷넷: 2368. C# / NAudio - (AI 학습을 위해) 무음 구간을 반영한 오디오 파일 분할파일 다운로드1
14021정성태10/6/20252630닷넷: 2367. C# - Youtube 동영상 다운로드 (YoutubeExplode 패키지) [1]파일 다운로드1
14020정성태10/2/20252275Linux: 125. eBPF - __attribute__((preserve_access_index)) 활용 사례
14019정성태10/1/20252408Linux: 124. eBPF - __sk_buff / sk_buff 구조체
14018정성태9/30/20251767닷넷: 2366. C# - UIAutomationClient를 이용해 시스템 트레이의 아이콘을 열거하는 방법파일 다운로드1
14017정성태9/29/20252237Linux: 123. eBPF (bpf2go) - BPF_PROG_TYPE_SOCKET_FILTER 예제 - SEC("socket")
14016정성태9/28/20252518Linux: 122. eBPF - __attribute__((preserve_access_index)) 사용법
14015정성태9/22/20251963닷넷: 2365. C# - FFMpegCore를 이용한 MP4 동영상으로부터 MP3 음원 추출 예제파일 다운로드1
14014정성태9/17/20251947닷넷: 2364. C# - stun.l.google.com을 사용해 공용 IP 주소와 포트를 알아내는 방법파일 다운로드1
14013정성태9/14/20252587닷넷: 2363. C# - Whisper.NET Library를 이용해 음성을 텍스트로 변환 및 번역하는 예제파일 다운로드1
14012정성태9/9/20252835닷넷: 2362. C# - Windows.Media.Ocr: 윈도우 운영체제에 포함된 OCR(Optical Character Recognition)파일 다운로드1
14011정성태9/7/20253460닷넷: 2361. C# - Linux 환경의 readlink 호출
14010정성태9/1/20253289오류 유형: 983. apt update 시 "The repository 'http://deb.debian.org/debian buster Release' does not have a Release file." 오류
14009정성태8/28/20253751닷넷: 2360. C# 14 - (11) Expression Tree에 선택적 인수와 명명된 인수 허용파일 다운로드1
14008정성태8/26/20254325닷넷: 2359. C# 14 - (10) 복합 대입 연산자의 오버로드 지원파일 다운로드1
14007정성태8/25/20254735닷넷: 2358. C# - 현재 빌드에 적용 중인 컴파일러 버전 확인 방법 (#error version)
14006정성태8/23/20255031Linux: 121. Linux - snap 패키지 관리자로 설치한 소프트웨어의 디렉터리 접근 제한
14005정성태8/21/20254006오류 유형: 982. sudo: unable to load /usr/libexec/sudo/sudoers.so: libssl.so.3: cannot open shared object file: No such file or directory
14004정성태8/21/20254595오류 유형: 981. dotnet 실행 시 No usable version of the libssl was found
14003정성태8/21/20254854닷넷: 2357. C# 14 - (9) 새로운 지시자 추가 (Ignored directives)
14002정성태8/20/20254990오류 유형: 980. C# - appsettings.json 파일의 설정값이 적용 안 된다면?
14001정성태8/19/20259773닷넷: 2356. .NET SDK 10 - 단일 소스 코드 파일을 빌드/실행하는 기능을 "dotnet" 명령어에 추가 [1]
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...