Microsoft MVP성태의 닷넷 이야기
.NET Framework: 999. C# - ArrayPool<T>와 MemoryPool<T> 소개 [링크 복사], [링크+제목 복사]
조회: 12159
글쓴 사람
정성태 (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)
13506정성태12/29/20232201닷넷: 2190. C# - 닷넷 코어/5+에서 달라지는 System.Text.Encoding 지원
13505정성태12/27/20232761닷넷: 2189. C# - WebSocket 클라이언트를 닷넷으로 구현하는 예제 (System.Net.WebSockets)파일 다운로드1
13504정성태12/27/20232332닷넷: 2188. C# - ASP.NET Core SignalR로 구현하는 채팅 서비스 예제파일 다운로드1
13503정성태12/27/20232195Linux: 67. WSL 환경 + mlocate(locate) 도구의 /mnt 디렉터리 검색 문제
13502정성태12/26/20232307닷넷: 2187. C# - 다른 프로세스의 환경변수 읽는 예제파일 다운로드1
13501정성태12/25/20232102개발 환경 구성: 700. WSL + uwsgi - IPv6로 바인딩하는 방법
13500정성태12/24/20232186디버깅 기술: 194. Windbg - x64 가상 주소를 물리 주소로 변환
13498정성태12/23/20232868닷넷: 2186. 한국투자증권 KIS Developers OpenAPI의 C# 래퍼 버전 - eFriendOpenAPI NuGet 패키지
13497정성태12/22/20232300오류 유형: 885. Visual Studiio - error : Could not connect to the remote system. Please verify your connection settings, and that your machine is on the network and reachable.
13496정성태12/21/20232319Linux: 66. 리눅스 - 실행 중인 프로세스 내부의 환경변수 설정을 구하는 방법 (gdb)
13495정성태12/20/20232328Linux: 65. clang++로 공유 라이브러리의 -static 옵션 빌드가 가능할까요?
13494정성태12/20/20232510Linux: 64. Linux 응용 프로그램의 (C++) so 의존성 줄이기(ReleaseMinDependency) - 두 번째 이야기
13493정성태12/19/20232607닷넷: 2185. C# - object를 QueryString으로 직렬화하는 방법
13492정성태12/19/20232302개발 환경 구성: 699. WSL에 nopCommerce 예제 구성
13491정성태12/19/20232239Linux: 63. 리눅스 - 다중 그룹 또는 사용자를 리소스에 권한 부여
13490정성태12/19/20232360개발 환경 구성: 698. Golang - GLIBC 의존을 없애는 정적 빌드 방법
13489정성태12/19/20232144개발 환경 구성: 697. GoLand에서 ldflags 지정 방법
13488정성태12/18/20232074오류 유형: 884. HTTP 500.0 - 명령행에서 실행한 ASP.NET Core 응용 프로그램을 실행하는 방법
13487정성태12/16/20232393개발 환경 구성: 696. C# - 리눅스용 AOT 빌드를 docker에서 수행 [1]
13486정성태12/15/20232207개발 환경 구성: 695. Nuget config 파일에 값 설정/삭제 방법
13485정성태12/15/20232093오류 유형: 883. dotnet build/restore - error : Root element is missing
13484정성태12/14/20232168개발 환경 구성: 694. Windows 디렉터리 경로를 WSL의 /mnt 포맷으로 구하는 방법
13483정성태12/14/20232307닷넷: 2184. C# - 하나의 resource 파일을 여러 프로그램에서 (AOT 시에도) 사용하는 방법파일 다운로드1
13482정성태12/13/20232887닷넷: 2183. C# - eFriend Expert OCX 예제를 .NET Core/5+ Console App에서 사용하는 방법 [2]파일 다운로드1
13481정성태12/13/20232275개발 환경 구성: 693. msbuild - .NET Core/5+ 프로젝트에서 resgen을 이용한 리소스 파일 생성 방법파일 다운로드1
13480정성태12/12/20232631개발 환경 구성: 692. Windows WSL 2 + Chrome 웹 브라우저 설치
1  2  3  4  [5]  6  7  8  9  10  11  12  13  14  15  ...