Microsoft MVP성태의 닷넷 이야기
.NET Framework: 999. C# - ArrayPool<T>와 MemoryPool<T> 소개 [링크 복사], [링크+제목 복사]
조회: 12283
글쓴 사람
정성태 (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

비밀번호

댓글 작성자
 




... 16  17  18  19  20  21  22  23  [24]  25  26  27  28  29  30  ...
NoWriterDateCnt.TitleFile(s)
13030정성태4/15/20226495오류 유형: 804. 정규 표현식 오류 - Quantifier {x,y} following nothing.
13029정성태4/14/20226899Windows: 203. iisreset 후에도 이전에 설정한 전역 환경 변수가 w3wp.exe에 적용되는 문제
13028정성태4/13/20226809.NET Framework: 1193. (appsettings.json처럼) web.config의 Debug/Release에 따른 설정 적용
13027정성태4/12/20227102.NET Framework: 1192. C# - 환경 변수의 변화를 알리는 WM_SETTINGCHANGE Win32 메시지 사용법파일 다운로드1
13026정성태4/11/20228633.NET Framework: 1191. C 언어로 작성된 FFmpeg Examples의 C# 포팅 전체 소스 코드 [3]
13025정성태4/11/20227966.NET Framework: 1190. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 vaapi_encode.c, vaapi_transcode.c 예제 포팅
13024정성태4/7/20226460.NET Framework: 1189. C# - 런타임 환경에 따라 달라진 AppDomain.GetCurrentThreadId 메서드
13023정성태4/6/20226765.NET Framework: 1188. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 transcoding.c 예제 포팅 [3]
13022정성태3/31/20226669Windows: 202. 윈도우 11 업그레이드 - "PC Health Check"를 통과했지만 여전히 업그레이드가 안 되는 경우 해결책
13021정성태3/31/20226839Windows: 201. Windows - INF 파일을 이용한 장치 제거 방법
13020정성태3/30/20226577.NET Framework: 1187. RDP 접속 시 WPF UserControl의 Unloaded 이벤트 발생파일 다운로드1
13019정성태3/30/20226574.NET Framework: 1186. Win32 Message를 Code로부터 메시지 이름 자체를 구하고 싶다면?파일 다운로드1
13018정성태3/29/20227116.NET Framework: 1185. C# - Unsafe.AsPointer가 반환한 포인터는 pinning 상태일까요? [5]
13017정성태3/28/20226916.NET Framework: 1184. C# - GC Heap에 위치한 참조 개체의 주소를 알아내는 방법 - 두 번째 이야기 [3]
13016정성태3/27/20227769.NET Framework: 1183. C# 11에 추가된 ref 필드의 (우회) 구현 방법파일 다운로드1
13015정성태3/26/20229122.NET Framework: 1182. C# 11 - ref struct에 ref 필드를 허용 [1]
13014정성태3/23/20227693VC++: 155. CComPtr/CComQIPtr과 Conformance mode 옵션의 충돌 [1]
13013정성태3/22/20226025개발 환경 구성: 641. WSL 우분투 인스턴스에 파이썬 2.7 개발 환경 구성하는 방법
13012정성태3/21/20225348오류 유형: 803. C# - Local '...' or its members cannot have their address taken and be used inside an anonymous method or lambda expression
13011정성태3/21/20226827오류 유형: 802. 윈도우 운영체제에서 웹캠 카메라 인식이 안 되는 경우
13010정성태3/21/20225751오류 유형: 801. Oracle.ManagedDataAccess.Core - GetTypes 호출 시 "Could not load file or assembly 'System.DirectoryServices.Protocols...'" 오류
13009정성태3/20/20227380개발 환경 구성: 640. docker - ibmcom/db2 컨테이너 실행
13008정성태3/19/20226674VS.NET IDE: 176. 비주얼 스튜디오 - 솔루션 탐색기에서 프로젝트를 선택할 때 csproj 파일이 열리지 않도록 만드는 방법
13007정성태3/18/20226259.NET Framework: 1181. C# - Oracle.ManagedDataAccess의 Pool 및 그것의 연결 개체 수를 알아내는 방법파일 다운로드1
13006정성태3/17/20227335.NET Framework: 1180. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 remuxing.c 예제 포팅
13005정성태3/17/20226199오류 유형: 800. C# - System.InvalidOperationException: Late bound operations cannot be performed on fields with types for which Type.ContainsGenericParameters is true.
... 16  17  18  19  20  21  22  23  [24]  25  26  27  28  29  30  ...