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

비밀번호

댓글 작성자
 




1  2  3  4  5  6  7  [8]  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13423정성태10/6/20233078스크립트: 58. 파이썬 - async/await 기본 사용법
13422정성태10/5/20233219닷넷: 2148. C# - async 유무에 따른 awaitable 메서드의 병렬 및 예외 처리
13421정성태10/4/20233247닷넷: 2147. C# - 비동기 메서드의 async 예약어 유무에 따른 차이
13420정성태9/26/20235338스크립트: 57. 파이썬 - UnboundLocalError: cannot access local variable '...' where it is not associated with a value
13419정성태9/25/20233107스크립트: 56. 파이썬 - RuntimeError: dictionary changed size during iteration
13418정성태9/25/20233781닷넷: 2146. C# - ConcurrentDictionary 자료 구조의 동기화 방식
13417정성태9/19/20233351닷넷: 2145. C# - 제네릭의 형식 매개변수에 속한 (매개변수를 가진) 생성자를 호출하는 방법
13416정성태9/19/20233161오류 유형: 877. redis-py - MISCONF Redis is configured to save RDB snapshots, ...
13415정성태9/18/20233641닷넷: 2144. C# 12 - 컬렉션 식(Collection Expressions)
13414정성태9/16/20233407디버깅 기술: 193. Windbg - ThreadStatic 필드 값을 조사하는 방법
13413정성태9/14/20233597닷넷: 2143. C# - 시스템 Time Zone 변경 시 이벤트 알림을 받는 방법
13412정성태9/14/20236871닷넷: 2142. C# 12 - 인라인 배열(Inline Arrays) [1]
13411정성태9/12/20233380Windows: 252. 권한 상승 전/후 따로 관리되는 공유 네트워크 드라이브 정보
13410정성태9/11/20234883닷넷: 2141. C# 12 - Interceptor (컴파일 시에 메서드 호출 재작성) [1]
13409정성태9/8/20233741닷넷: 2140. C# - Win32 API를 이용한 모니터 전원 끄기
13408정성태9/5/20233730Windows: 251. 임의로 만든 EXE 파일을 포함한 ZIP 파일의 압축을 해제할 때 Windows Defender에 의해 삭제되는 경우
13407정성태9/4/20233488닷넷: 2139. C# - ParallelEnumerable을 이용한 IEnumerable에 대한 병렬 처리
13406정성태9/4/20233443VS.NET IDE: 186. Visual Studio Community 버전의 라이선스
13405정성태9/3/20233869닷넷: 2138. C# - async 메서드 호출 원칙
13404정성태8/29/20233394오류 유형: 876. Windows - 키보드의 등호(=, Equals sign) 키가 눌리지 않는 경우
13403정성태8/21/20233223오류 유형: 875. The following signatures couldn't be verified because the public key is not available: NO_PUBKEY EB3E94ADBE1229CF
13402정성태8/20/20233289닷넷: 2137. ILSpy의 nuget 라이브러리 버전 - ICSharpCode.Decompiler
13401정성태8/19/20233526닷넷: 2136. .NET 5+ 환경에서 P/Invoke의 성능을 높이기 위한 SuppressGCTransition 특성 [1]
13400정성태8/10/20233351오류 유형: 874. 파이썬 - pymssql을 윈도우 환경에서 설치 불가
13399정성태8/9/20233375닷넷: 2135. C# - 지역 변수로 이해하는 메서드 매개변수의 값/참조 전달
13398정성태8/3/20234135스크립트: 55. 파이썬 - pyodbc를 이용한 SQL Server 연결 사용법
1  2  3  4  5  6  7  [8]  9  10  11  12  13  14  15  ...