Microsoft MVP성태의 닷넷 이야기
.NET Framework: 995. C# - Span<T>와 Memory<T> [링크 복사], [링크+제목 복사],
조회: 22132
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 3개 있습니다.)

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분
정성태

... [121]  122  123  124  125  126  127  128  129  130  131  132  133  134  135  ...
NoWriterDateCnt.TitleFile(s)
10900정성태2/19/201622083.NET Framework: 548. Linq는 결국 메서드 호출! [3]파일 다운로드1
10899정성태2/17/201623400개발 환경 구성: 282. kernel32.dll, kernel32legacy.dll, api-ms-win-core-sysinfo-l1-2-0.dll [1]
10898정성태2/17/201621918.NET Framework: 547. PerformanceCounter의 InstanceName 지정 시 주의 사항파일 다운로드1
10897정성태2/17/201621271디버깅 기술: 76. windbg 분석 사례 - 닷넷 프로파일러의 GC 콜백 부하
10896정성태2/17/201622383오류 유형: 320. FATAL: 28000: no pg_hba.conf entry for host "fe80::1970:8120:695:a41e%12"
10895정성태2/17/201621201.NET Framework: 546. System.AppDomain으로부터 .NET Profiler의 AppDomainID 구하는 방법 [1]
10894정성태2/17/201621889오류 유형: 319. Visual Studio에서 찾기는 성공하지만 해당 소스 코드 정보가 보이지 않는 경우
10893정성태2/16/201620606.NET Framework: 545. 닷넷 - 특정 클래스가 로드되었는지 여부를 알 수 있을까? - 두 번째 이야기
10892정성태2/16/201621146오류 유형: 318. 탐색기에서 폴더 생성/삭제 시 몇 초 동안 멈추는 현상
10891정성태2/16/201624219VC++: 95. 내 CPU가 MPX/SGX를 지원할까요? [1]
10890정성태2/15/201624076.NET Framework: 544. C# 5의 Caller Info를 .NET 4.5 미만의 응용 프로그램에 적용하는 방법 [5]
10889정성태2/14/201620402.NET Framework: 543. C++의 inline asm 사용을 .NET으로 포팅하는 방법 - 두 번째 이야기파일 다운로드1
10888정성태2/14/201618714.NET Framework: 542. 닷넷 - 특정 클래스가 로드되었는지 여부를 알 수 있을까?
10887정성태2/3/201619418VC++: 94. MPX(Memory Protection Extensions) 테스트파일 다운로드1
10886정성태2/3/201620658개발 환경 구성: 281. Intel MPX Runtime Driver 수동 설치
10885정성태2/2/201620353오류 유형: 317. Sybase.Data.AseClient.AseException: The command has timed out.
10884정성태1/11/201621575개발 환경 구성: 280. 닷넷에서 SAP Adaptive Server Enterprise 데이터베이스 사용파일 다운로드1
10882정성태1/6/201620869Windows: 113. 윈도우의 2179, 26143, 47001 TCP 포트 사용 [1]
10881정성태1/3/201622230오류 유형: 316. 윈도우 10 - 바탕/돋음 체가 사라져 한글이 깨지는 현상 [2]
10880정성태12/16/201519998오류 유형: 315. 닷넷 프로파일러의 오류 코드 정보
10879정성태12/16/201521948오류 유형: 314. Error : DEP0700 : Registration of the app failed. error 0x80070005
10878정성태12/9/201524994디버깅 기술: 75. UWP(유니버설 윈도우 플랫폼) 앱에서 global::System.Diagnostics.Debugger.Break 예외 발생 시 대응 방법
10877정성태12/9/201529367VC++: 93. std::thread 사용 시 R6010 오류 [2]
10876정성태11/26/201525489.NET Framework: 541. SignedXml을 이용한 ds:Signature만드는 방법 [3]파일 다운로드1
10875정성태11/26/201530442개발 환경 구성: 279. signtool.exe의 다중 서명 기능 [2]
10874정성태11/26/201526413개발 환경 구성: 278. 인증서와 인증서를 이용한 코드 사인의 해시 구분
... [121]  122  123  124  125  126  127  128  129  130  131  132  133  134  135  ...