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

.NET Framework: 999. C# - ArrayPool<T>와 MemoryPool<T> 소개
; https://www.sysnet.pe.kr/2/0/12480

.NET Framework: 1124. C# - .NET Platform Extension의 ObjectPool<T> 사용법 소개
; https://www.sysnet.pe.kr/2/0/12893

.NET Framework: 1125. C# - DefaultObjectPool<T>의 IDisposable 개체에 대한 풀링 문제
; https://www.sysnet.pe.kr/2/0/12894




C# - ArrayPool<T>와 MemoryPool<T> 소개

일단, ArrayPool<T>는 이전 글에서 소개했으니,

C# - ArrayPool<T> 소개
; https://www.sysnet.pe.kr/2/0/12478

이번에는 MemoryPool<T>에 대한 부연 설명을 해보겠습니다. ^^




사실 MemoryPool<T>는 ArrayPool<T>를 포함하는 구조입니다. 따라서 ArrayPool<T>를 구현했던 기존 소스 코드를 MemoryPool<T>를 사용해 변환할 수 있습니다.

{
    IMemoryOwner<byte> buffer = MemoryPool<byte>.Shared.Rent(1024);
    Console.WriteLine($"buffer[0] == {buffer.Memory.Span[0]}");
    buffer.Memory.Span[0] = 5;
    buffer.Dispose(); // ArrayPool의 Return 대신 IMemoryOwner.Dispose로 반환

    buffer = MemoryPool<byte>.Shared.Rent(1024);
    Console.WriteLine($"buffer[0] == {buffer.Memory.Span[0]}");
    buffer.Dispose();
}

Console.WriteLine();

{
    // using 사용 가능
    using (IMemoryOwner<byte> buffer = MemoryPool<byte>.Shared.Rent(1000))
    {
        Console.WriteLine(buffer.Memory.Length);
    }

    using (var buffer = MemoryPool<byte>.Shared.Rent(513))
    {
        Console.WriteLine(buffer.Memory.Length);
    }

    {
        using var buffer = MemoryPool<byte>.Shared.Rent(512);
        Console.WriteLine(buffer.Memory.Length);
    }
}

/* 출력 결과
buffer[0] == 0
buffer[0] == 5

1024
1024
512
*/

보다시피 약간의 차이점이 있는데, 1) Rent는 직접적인 배열이 아닌 IMemoryOwner를 구현한 타입을 반환하고, 2) 따라서 IMemoryOwner.Memory 속성을 통해 요소를 접근해야 하며, 3) Pool에 반환하는 방법은 IMemoryOwner.Dispose 메서드 호출로 이뤄집니다.

기본 구현된 MemoryPool<T>.Shared의,

static MemoryPool()
{
    MemoryPool<T>.s_shared = new ArrayMemoryPool<T>();
}

ArrayMemoryPool 타입이 구현한 Rent 메서드는,

public sealed override IMemoryOwner<T> Rent(int minimumBufferSize = -1)
{
    if (minimumBufferSize == -1)
    {
        minimumBufferSize = 1 + 4095 / Unsafe.SizeOf<T>();
    }
    else if (minimumBufferSize > 2147483647)
    {
        ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.minimumBufferSize);
    }
    return new ArrayMemoryPool<T>.ArrayMemoryPoolBuffer(minimumBufferSize);
}

ArrayMemoryPoolBuffer 인스턴스를 반환하는데,

private sealed class ArrayMemoryPoolBuffer : IMemoryOwner<T>, IDisposable
{
    public ArrayMemoryPoolBuffer(int size)
    {
        this._array = ArrayPool<T>.Shared.Rent(size);
    }

    public Memory<T> Memory
    {
        get
        {
            T[] array = this._array;
            if (array == null)
            {
                ThrowHelper.ThrowObjectDisposedException_ArrayMemoryPoolBuffer();
            }
            return new Memory<T>(array);
        }
    }

    public void Dispose()
    {
        T[] array = this._array;
        if (array != null)
        {
            this._array = null;
            ArrayPool<T>.Shared.Return(array, false);
        }
    }

    private T[] _array;
}

결국 ArrayPool<T>.Shared의 구현을 기반으로 합니다. 이와 함께 또 한가지 차이점이 있다면, ArrayPool<T>와는 다르게 Shared가 아닌 별도의 Pool을 생성하는 Create 메서드를 제공하지 않는데요. 아마도 그 수요가 거의 없다고 판단했을 수 있고, 게다가 MemoryPool<T> 타입 자체가 추상 타입이기 때문에 굳이 그 정도까지 원한다면 상속을 이용하면 된다...라는 의도인 듯합니다.

참고로, IMemoryOwner<T>.Memory 속성의 타입이 전에 설명한 Memory<T>입니다.

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




그럼 ETW 지원은 어떨까요? MemoryPool<T> 자체는 ETW에 대한 지원을 하고 있지 않지만, Shared 속성의 내부에서 사용하는 타입이 ArrayPool이므로 그 수준에서 ETW 이벤트가 제공됩니다. 따라서 소스 코드는 이전 글의 것과 동일하게 구현할 수 있습니다.

using System;
using System.Buffers;
using System.Diagnostics.Tracing;
using System.Threading;

namespace ConsoleApp2
{
    class Program
    {
        static MyEventListener _listener;

        // https://www.sysnet.pe.kr/2/0/12473
        static Program()
        {
            _listener = new MyEventListener();
        }

        static void Main(string[] args)
        {
            Console.WriteLine($"Main TID: {Thread.CurrentThread.ManagedThreadId}");

            using var buffer = MemoryPool<byte>.Shared.Rent(4000);
            Console.WriteLine($", BufferLen: {buffer.Memory.Length}");
        }
    }
}

internal class MyEventListener : EventListener
{
    // ...[생략: https://www.sysnet.pe.kr/2/0/12478#etw_src]...
}




위에서, MemoryPool을 상속받아 사용자 정의할 수 있다고 했는데, 간단하게는 BCL에서 제공하는 구조를 그대로 베껴 다음과 같이 구현할 수 있습니다.

public abstract class CMemoryPool<T> : MemoryPool<T>
{
    public new static MemoryPool<T> Shared
    {
        get
        {
            return CMemoryPool<T>.s_shared;
        }
    }

    private static readonly CArrayMemoryPool<T> s_shared = new CArrayMemoryPool<T>();
}

internal class CArrayMemoryPool<T> : MemoryPool<T>
{
    public override int MaxBufferSize
    {
        get
        {
            return 2147483647;
        }
    }

    public override IMemoryOwner<T> Rent(int minimumBufferSize = -1)
    {
        if (minimumBufferSize > 2147483647)
        {
            throw new ArgumentException($"{minimumBufferSize} > {MaxBufferSize}");
        }

        return new CArrayMemoryPool<T>.CArrayMemoryPoolBuffer(minimumBufferSize);
    }

    protected override void Dispose(bool disposing)
    {
    }

    private class CArrayMemoryPoolBuffer : IMemoryOwner<T>, IDisposable
    {
        private T[] _array;

        public CArrayMemoryPoolBuffer(int size)
        {
            this._array = ArrayPool<T>.Shared.Rent(size);
        }

        public Memory<T> Memory
        {
            get
            {
                T[] array = this._array;

                if (array == null)
                {
                    throw new NullReferenceException();
                }

                return new Memory<T>(array);
            }
        }

        public void Dispose()
        {
            T[] array = this._array;
            if (array != null)
            {
                this._array = null;
                ArrayPool<T>.Shared.Return(array, false);
            }
        }
    }
}

자, 그럼 위의 코드에 부가 기능을 넣어볼까요? 그러고 보니, 이 글의 첫 번째 예제에서 MemoryPool에 반환한 버퍼가 그대로 재사용되어 이전 값이 남아 있는 것을 볼 수 있는데요, ArrayPool과는 달리 반환 시 "clearArray"를 제어할 수 없어 사용자가 직접 관련 기능을 작성해야만 합니다. 하지만, 위와 같이 코드를 만들었으니, CArrayMemoryPoolBuffer<T>.Dispose 메서드에서 반환 시 clearArray 옵션을 제어하는 값을 변경해 처리하면 개발자의 실수를 예방할 수 있습니다.

internal class CArrayMemoryPool<T> : MemoryPool<T>
{
    // ...[생략]...

    public void Dispose()
    {
        T[] array = this._array;
        if (array != null)
        {
            this._array = null;
            ArrayPool<T>.Shared.Return(array, true);
        }
    }

    // ...[생략]...
}

하나 더 기능을 넣어 볼까요? ^^ 가만 보니까, IMemoryOwner<T>.Memory 속성의 타입이 Memory<T>였다고 했으니 반환받은 메모리에 대한 구역을 제어하는 것이 가능합니다. ArrayPool<T>의 경우에는 T [] 타입을 반환하므로 그것이 불가능했지만, Span<T>를 담고 있는 Memory<T>라면 다음과 같이 쉽게 제어 코드를 넣을 수 있습니다.

private class CArrayMemoryPoolBuffer : IMemoryOwner<T>, IDisposable
{
    int _size;
    private T[] _array;

    public CArrayMemoryPoolBuffer(int size)
    {
        _size = size;
        this._array = ArrayPool<T>.Shared.Rent(size);
    }

    public Memory<T> Memory
    {
        get
        {
            T[] array = this._array;

            if (array == null)
            {
                throw new NullReferenceException();
            }

            return new Memory<T>(array, 0, _size);
        }
    }

    // ...[생략]...
}

따라서, 이렇게 변경한 MemoryPool 타입을 사용하면 다음과 같은 결과를 얻을 수 있습니다.

{
    IMemoryOwner<byte> buffer = CMemoryPool<byte>.Shared.Rent(1024);
    Console.WriteLine($"buffer[0] == {buffer.Memory.Span[0]}");
    buffer.Memory.Span[0] = 5;
    buffer.Dispose();

    buffer = CMemoryPool<byte>.Shared.Rent(1024);
    Console.WriteLine($"buffer[0] == {buffer.Memory.Span[0]}");
    buffer.Dispose();
}

Console.WriteLine();

{
    using (IMemoryOwner<byte> buffer = CMemoryPool<byte>.Shared.Rent(1000))
    {
        Console.WriteLine(buffer.Memory.Length);
    }

    using (var buffer = CMemoryPool<byte>.Shared.Rent(513))
    {
        Console.WriteLine(buffer.Memory.Length);
    }

    {
        using var buffer = CMemoryPool<byte>.Shared.Rent(512);
        Console.WriteLine(buffer.Memory.Length);
    }
}
/* 출력 결과
buffer[0] == 0
buffer[0] == 0

1000
513
512
*/

또는, 아예 Marshal.AllocHGlobal / Marshal.FreeHGlobal을 사용하여,

C# - 고성능이 필요한 환경에서 GC가 발생하지 않는 네이티브 힙 사용
; https://www.sysnet.pe.kr/2/0/12036

GC 힙을 전혀 사용하지 않는 MemoryPool을 만드는 것도 가능할 것입니다.




이렇게 자유로운 커스터마이징이 가능하지만, 대개의 경우 MemoryPool보다는 ArrayPool을 사용하는 것이 좋습니다. 이와 관련해 좋은 사례가 있는데요,

ArrayPool vs MemoryPool-minimizing allocations in AIS.NET
; https://endjin.com/blog/2020/09/arraypool-vs-memorypool-minimizing-allocations-ais-dotnet

내용이 많지만, 결국 정리해 보면 MemoryPool<T>는 Rent 시 (IMemoryOwner를 구현한) 부가 개체(x64 - 24바이트의 System.Buffers.ArrayMemoryPool`1+ArrayMemoryPoolBuffer)의 사용으로 인해 GC Heap이 사용되는 문제가 있어 ArrayPool<T>로 바꿔 해결했다는 것입니다. 사실, 벤치마킹 테스트를 할 정도의 고부하에서도 MemoryPool<T> 사용 시 13MB 정도가 사용된 거라면 일반적인 응용 프로그램에서는 거의 무시해도 될 정도입니다. 단지, 해당 라이브러리가 저사양 기기에서도 사용할 수 있도록 하는 것이 목표여서 저렇게까지 할 필요가 있다고 언급하고 있습니다.

따라서, MemoryPool<T>가 부가 개체를 생성하는 반면 유연성은 있지만 ArrayPool<T>은 그 반대의 효과를 갖는다는 차이점으로 정리할 수 있습니다.

이외에 관심이 있다면 MemoryOwner<T>도 보시면 좋겠고. ^^

MemoryOwner
; https://learn.microsoft.com/en-us/windows/communitytoolkit/high-performance/memoryowner

MemoryOwner Class
; https://learn.microsoft.com/en-us/dotnet/api/microsoft.toolkit.highperformance.buffers.memoryowner-1?view=win-comm-toolkit-dotnet-stable




.NET Framework의 경우 MemoryPool 타입이 기본 BCL에 없으므로,

MemoryPool<T> Class
; https://learn.microsoft.com/en-us/dotnet/api/system.buffers.memorypool-1

최소 요구 사항: .NET 5.0, .NET Core 2.1, .NET Standard 2.1

Nuget에서 제공하는 별도 라이브러리를 참조해야 합니다.

System.Memory
; https://www.nuget.org/packages/System.Memory/
(.NET Framework 4.5부터 참조 가능합니다.)
Install-Package System.Memory

이하 사용법은 .NET Core의 것과 동일합니다.




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




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 3/19/2023]

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

비밀번호

댓글 작성자
 




... 31  32  33  34  35  36  37  38  39  40  41  [42]  43  44  45  ...
NoWriterDateCnt.TitleFile(s)
12589정성태4/8/202110575.NET Framework: 1032. C# - Environment.OSVersion의 문제점 및 윈도우 운영체제의 버전을 구하는 다양한 방법 [1]
12588정성태4/7/202111125개발 환경 구성: 565. PowerShell - New-SelfSignedCertificate를 사용해 CA 인증서 생성 및 인증서 서명 방법
12587정성태4/6/202111959개발 환경 구성: 564. Windows 10 - ClickOnce 배포처럼 사용할 수 있는 MSIX 설치 파일 [1]
12586정성태4/5/20219655오류 유형: 710. Windows - Restart-Computer / shutdown 명령어 수행 시 Access is denied(E_ACCESSDENIED)
12585정성태4/5/20219368개발 환경 구성: 563. 기본 생성된 kubeconfig 파일의 내용을 새롭게 생성한 인증서로 구성하는 방법
12584정성태4/1/202110086개발 환경 구성: 562. kubeconfig 파일 없이 kubectl 옵션만으로 실행하는 방법
12583정성태3/29/202111536개발 환경 구성: 561. kubectl 수행 시 다른 k8s 클러스터로 접속하는 방법
12582정성태3/29/202110268오류 유형: 709. Visual C++ - 컴파일 에러 error C2059: syntax error: '__stdcall'
12581정성태3/28/202110175.NET Framework: 1031. WinForm/WPF에서 Console 창을 띄워 출력하는 방법 (2) - Output 디버깅 출력을 AllocConsole로 우회 [2]
12580정성태3/28/20218896오류 유형: 708. SQL Server Management Studio - Execution Timeout Expired.
12579정성태3/28/20218992오류 유형: 707. 중첩 가상화(Nested Virtualization) - The virtual machine could not be started because this platform does not support nested virtualization.
12578정성태3/27/20219255개발 환경 구성: 560. Docker Desktop for Windows 기반의 Kubernetes 구성 (2) - WSL 2 인스턴스에 kind가 구성한 k8s 서비스 위치
12577정성태3/26/202111310개발 환경 구성: 559. Docker Desktop for Windows 기반의 Kubernetes 구성 - WSL 2 인스턴스에 kind 도구로 k8s 클러스터 구성
12576정성태3/25/20219105개발 환경 구성: 558. Docker Desktop for Windows에서 DockerDesktopVM 기반의 Kubernetes 구성 (2) - k8s 서비스 위치
12575정성태3/24/20218160개발 환경 구성: 557. Docker Desktop for Windows에서 DockerDesktopVM 기반의 Kubernetes 구성
12574정성태3/23/202112236.NET Framework: 1030. C# Socket의 Close/Shutdown 동작 (동기 모드)
12573정성태3/22/202110011개발 환경 구성: 556. WSL 인스턴스 초기 설정 명령어 [1]
12572정성태3/22/20219556.NET Framework: 1029. C# - GC 호출로 인한 메모리 압축(Compaction)을 확인하는 방법파일 다운로드1
12571정성태3/21/20218708오류 유형: 706. WSL 2 기반으로 "Enable Kubernetes" 활성화 시 초기화 실패 [1]
12570정성태3/19/202113010개발 환경 구성: 555. openssl - CA로부터 인증받은 새로운 인증서를 생성하는 방법
12569정성태3/18/202111965개발 환경 구성: 554. WSL 인스턴스 export/import 방법 및 단축 아이콘 설정 방법
12568정성태3/18/20217498오류 유형: 705. C# 빌드 - Couldn't process file ... due to its being in the Internet or Restricted zone or having the mark of the web on the file.
12567정성태3/17/20218928개발 환경 구성: 553. Docker Desktop for Windows를 위한 k8s 대시보드 활성화 [1]
12566정성태3/17/20219283개발 환경 구성: 552. Kubernetes - kube-apiserver와 REST API 통신하는 방법 (Docker Desktop for Windows 환경)
12565정성태3/17/20216735오류 유형: 704. curl.exe 실행 시 dll not found 오류
12564정성태3/16/20217209VS.NET IDE: 160. 새 프로젝트 창에 C++/CLI 프로젝트 템플릿이 없는 경우
... 31  32  33  34  35  36  37  38  39  40  41  [42]  43  44  45  ...