Microsoft MVP성태의 닷넷 이야기
.NET Framework: 999. C# - ArrayPool<T>와 MemoryPool<T> 소개 [링크 복사], [링크+제목 복사]
조회: 796
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

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://docs.microsoft.com/en-us/windows/communitytoolkit/high-performance/memoryowner

MemoryOwner Class
; https://docs.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://docs.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의 것과 동일합니다.




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




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

[연관 글]


donaricano-btn



[최초 등록일: ]
[최종 수정일: 1/9/2021 ]

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)
12404정성태11/22/2020951.NET Framework: 964. C# 9.0 - (13) 모듈 이니셜라이저(Module initializers)파일 다운로드1
12403정성태11/22/20201017.NET Framework: 963. C# 9.0 - (12) foreach 루프에 대한 GetEnumerator 확장 메서드 지원(Extension GetEnumerator)파일 다운로드1
12402정성태11/22/20201263.NET Framework: 962. C# 9.0 - (11) 공변 반환 형식(Covariant return types) [1]파일 다운로드1
12401정성태11/5/2020848VS.NET IDE: 153. 닷넷 응용 프로그램에서의 "My Code" 범위와 "Enable Just My Code"의 역할
12400정성태11/5/2020790오류 유형: 679. Visual Studio - "Source Not Found" 창에 "Decompile source code" 링크가 없는 경우
12399정성태11/22/2020927.NET Framework: 961. C# 9.0 - (10) 대상으로 형식화된 조건식(Target-typed conditional expressions)파일 다운로드1
12398정성태11/4/2020814오류 유형: 678. Windows Server 2008 R2 환경에서 Powershell을 psexec로 원격 실행할 때 hang이 발생하는 문제
12397정성태11/4/2020849.NET Framework: 960. C# - 조건 연산자(?:)를 사용하는 경우 달라지는 메서드 선택 사례파일 다운로드1
12396정성태11/3/2020825VS.NET IDE: 152. Visual Studio - "Tools" / "External Tools..."에 등록된 외부 명령어에 대한 단축키 설정 방법
12395정성태11/3/2020599오류 유형: 677. SSMS로 DB 접근 시 The server principal "..." is not able to access the database "..." under the current security context.
12394정성태11/3/2020656오류 유형: 676. cacls - The Recycle Bin on ... is corrupted. Do you want to empty the Recycle Bin for this drive?
12393정성태11/3/2020861오류 유형: 675. Visual Studio - 닷넷 응용 프로그램 디버깅 시 Disassembly 창에서 BP 설정할 때 "Error while processing breakpoint." 오류
12392정성태11/22/20201284.NET Framework: 959. C# 9.0 - (9) 레코드(Records) [3]파일 다운로드1
12390정성태11/1/2020941디버깅 기술: 173. windbg - System.Configuration.ConfigurationErrorsException 예외 분석 방법
12389정성태11/22/20201078.NET Framework: 958. C# 9.0 - (8) 정적 익명 함수 (static anonymous functions)파일 다운로드1
12388정성태10/29/2020901오류 유형: 674. 어느 순간부터 닷넷 응용 프로그램 실행 시 System.Configuration.ConfigurationErrorsException 예외가 발생한다면?
12387정성태10/29/20201084.NET Framework: 957. C# - static 필드의 정보가 GC Heap에 저장될까요? [3]파일 다운로드1
12386정성태10/28/20201021Linux: 34. 사용자 정보를 함께 출력하는 리눅스의 ps 명령어 사용 방법
12385정성태10/28/2020704오류 유형: 673. openssl - req: No value provided for Subject Attribute CN, skipped
12384정성태10/27/2020811오류 유형: 672. AllowPartiallyTrustedCallers 특성이 적용된 어셈블리의 struct 멤버 메서드를 재정의하면 System.Security.VerificationException 예외 발생
12383정성태11/22/20201164.NET Framework: 956. C# 9.0 - (7) 패턴 일치 개선 사항(Pattern matching enhancements) [1]파일 다운로드1
12382정성태11/13/2020903오류 유형: 671. dotnet build - The local source '...' doesn't exist
12381정성태10/26/2020972VC++: 137. C++ stl map의 사용자 정의 타입을 key로 사용하는 방법파일 다운로드1
12380정성태10/26/2020658오류 유형: 670. Visual Studio - Squash_FailureCommitsReset
12379정성태10/26/20201101.NET Framework: 955. .NET 메서드의 Signature 바이트 코드 분석파일 다운로드2
12378정성태10/20/20201133.NET Framework: 954. C# - x86/x64 환경에 따라 달라지는 P/Invoke 함수의 export 이름파일 다운로드1
1  2  3  4  5  6  7  8  [9]  10  11  12  13  14  15  ...