Microsoft MVP성태의 닷넷 이야기
.NET Framework: 995. C# - Span<T>와 Memory<T> [링크 복사], [링크+제목 복사],
조회: 22166
글쓴 사람
정성태 (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)
1831정성태1/1/201528533디버깅 기술: 66. windbg 분석 사례 - cpu 100% 현상 (1) [1]
1830정성태1/1/201527572오류 유형: 265. svchost.exe 프로세스(IP Helper: IPHLPSVC)의 CPU 100% 현상
1829정성태12/16/201431340VC++: 86. Windows Vista부터 바뀐 Credential Provider 예제 분석 (2) [2]파일 다운로드1
1828정성태12/15/201427804VC++: 85. Windows Vista부터 바뀐 Credential Provider 예제 분석 (1) [4]파일 다운로드1
1827정성태12/12/201424155VC++: 84. CredUIPromptForWindowsCredentials Win32 API 사용법 정리
1826정성태12/11/201428629.NET Framework: 489. Socket.Listen에 전달된 backlog 인자의 의미 [6]
1825정성태12/11/201475796.NET Framework: 488. TCP 소켓 연결의 해제를 알 수 있는 방법 [10]파일 다운로드1
1824정성태12/10/201425850.NET Framework: 487. Socket.Receive 메서드의 SocketFlags.Peek 동작을 이용해 소켓 연결 유무를 확인? [8]파일 다운로드1
1823정성태12/10/201422797.NET Framework: 486. Java의 ScheduledExecutorService에 대응하는 C#의 System.Threading.Timer [2]
1822정성태12/3/201425440개발 환경 구성: 252. Xamarin 라이선스 관리 [8]
1821정성태12/1/201431881VS.NET IDE: 96. Visual Studio에서 /* ~ */ 주석을 위한 단축키(매크로)
1820정성태11/23/201420603.NET Framework: 485. Mono Profiler에서 IL 코드 변경이 가능할까? - 두 번째 이야기파일 다운로드1
1819정성태11/22/201420598오류 유형: 264. MSBuild 에러 - warning MSB3052: The parameter to the compiler is invalid,
1818정성태11/22/201423012개발 환경 구성: 251. NUMA 노드 수에 따른 IIS Worker Process(w3wp.exe) 설정 [1]
1817정성태11/22/201426165VS.NET IDE: 95. Macros for Visual Studio 2013 [1]
1816정성태11/20/201420853.NET Framework: 484. Mono Profiler에서 IL 코드 변경이 가능할까?
1815정성태11/18/201418854.NET Framework: 483. 코드로 살펴 보는 ETW의 활성화 시점 [2]
1814정성태11/18/201420391오류 유형: 263. Unable to find the requested .Net Framework Data Provider. It may not be installed.
1813정성태11/18/201422212오류 유형: 262. Build Events에 robocopy 작업이 있는 경우 "VCEnd exited with code 3" 오류 발생
1812정성태11/17/201419675.NET Framework: 482. ETW 자동 생성 코드의 VerificationException 예외 발생
1811정성태11/16/201420998.NET Framework: 481. Mono 내부의 문자열 처리 방식은 UTF-8
1810정성태11/15/201457647.NET Framework: 480. C# - 배치 파일 실행하고 출력 결과를 얻는 방법 [4]
1809정성태11/11/201422892.NET Framework: 479. Mono Profiler를 Unity의 Plugin으로 사용하는 방법
1808정성태11/11/201423218.NET Framework: 478. C# - 폴더 경로 문자열에서 "..", "." 표기를 고려한 최종 문자열을 얻는 방법 [2]
1807정성태11/10/201429349개발 환경 구성: 250. 서버용 Socket에서 사용하는 포트가 충돌한다면?파일 다운로드1
1806정성태11/10/201425135.NET Framework: 477. SeCreateGlobalPrivilege 특권과 WCF NamedPipe
... 121  122  123  124  125  126  127  128  [129]  130  131  132  133  134  135  ...