성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] VT sequences to "CONOUT$" vs. STD_O...
[정성태] NetCoreDbg is a managed code debugg...
[정성태] Evaluating tail call elimination in...
[정성태] What’s new in System.Text.Json in ....
[정성태] What's new in .NET 9: Cryptography ...
[정성태] 아... 제시해 주신 "https://akrzemi1.wordp...
[정성태] 다시 질문을 정리할 필요가 있을 것 같습니다. 제가 본문에...
[이승준] 완전히 잘못 짚었습니다. 댓글 지우고 싶네요. 검색을 해보...
[정성태] 우선 답글 감사합니다. ^^ 그런데, 사실 저 예제는 (g...
[이승준] 수정이 안되어서... byteArray는 BYTE* 타입입니다...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>C# - ArrayPool<T>와 MemoryPool<T> 소개</h1> <p> 일단, <a target='tab' href='https://learn.microsoft.com/en-us/dotnet/api/system.buffers.arraypool-1'>ArrayPool<T></a>는 이전 글에서 소개했으니,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > C# - ArrayPool<T> 소개 ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12478'>https://www.sysnet.pe.kr/2/0/12478</a> </pre> <br /> 이번에는 <a target='tab' href='https://learn.microsoft.com/en-us/dotnet/api/system.buffers.memorypool-1'>MemoryPool<T></a>에 대한 부연 설명을 해보겠습니다. ^^<br /> <br /> <hr style='width: 50%' /><br /> <br /> 사실 MemoryPool<T>는 ArrayPool<T>를 포함하는 구조입니다. 따라서 ArrayPool<T>를 구현했던 기존 소스 코드를 MemoryPool<T>를 사용해 변환할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > { <span style='color: blue; font-weight: bold'>IMemoryOwner<byte> buffer</span> = MemoryPool<byte>.Shared.Rent(1024); Console.WriteLine($"buffer[0] == {<span style='color: blue; font-weight: bold'>buffer.Memory.Span</span>[0]}"); buffer.Memory.Span[0] = 5; <span style='color: blue; font-weight: bold'>buffer.Dispose</span>(); // ArrayPool의 Return 대신 IMemoryOwner.Dispose로 반환 buffer = MemoryPool<byte>.Shared.Rent(1024); Console.WriteLine($"buffer[0] == {buffer.Memory.Span[0]}"); buffer.Dispose(); } Console.WriteLine(); { // using 사용 가능 using (<span style='color: blue; font-weight: bold'>IMemoryOwner<byte></span> buffer = MemoryPool<byte>.Shared.<span style='color: blue; font-weight: bold'>Rent</span>(1000)) { Console.WriteLine(buffer.Memory.Length); } using (<span style='color: blue; font-weight: bold'>var buffer</span> = MemoryPool<byte>.Shared.<span style='color: blue; font-weight: bold'>Rent</span>(513)) { Console.WriteLine(buffer.Memory.Length); } { <span style='color: blue; font-weight: bold'>using</span> var buffer = MemoryPool<byte>.Shared.<span style='color: blue; font-weight: bold'>Rent</span>(512); Console.WriteLine(buffer.Memory.Length); } } /* 출력 결과 buffer[0] == 0 buffer[0] == 5 1024 1024 512 */ </pre> <br /> 보다시피 약간의 차이점이 있는데, 1) Rent는 직접적인 배열이 아닌 IMemoryOwner를 구현한 타입을 반환하고, 2) 따라서 IMemoryOwner.Memory 속성을 통해 요소를 접근해야 하며, 3) Pool에 반환하는 방법은 IMemoryOwner.Dispose 메서드 호출로 이뤄집니다.<br /> <br /> 기본 구현된 MemoryPool<T>.Shared의,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > static MemoryPool() { MemoryPool<T>.s_shared = new <span style='color: blue; font-weight: bold'>ArrayMemoryPool</span><T>(); } </pre> <br /> ArrayMemoryPool 타입이 구현한 Rent 메서드는,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > public sealed override IMemoryOwner<T> <span style='color: blue; font-weight: bold'>Rent</span>(int minimumBufferSize = -1) { if (minimumBufferSize == -1) { minimumBufferSize = 1 + 4095 / Unsafe.SizeOf<T>(); } else if (minimumBufferSize > 2147483647) { ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.minimumBufferSize); } return <span style='color: blue; font-weight: bold'>new ArrayMemoryPool<T>.ArrayMemoryPoolBuffer</span>(minimumBufferSize); } </pre> <br /> ArrayMemoryPoolBuffer 인스턴스를 반환하는데,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > private sealed class ArrayMemoryPoolBuffer : IMemoryOwner<T>, IDisposable { public ArrayMemoryPoolBuffer(int size) { this._array = <span style='color: blue; font-weight: bold'>ArrayPool<T>.Shared.Rent(size);</span> } public Memory<T> Memory { get { T[] array = this._array; if (array == null) { ThrowHelper.ThrowObjectDisposedException_ArrayMemoryPoolBuffer(); } return <span style='color: blue; font-weight: bold'>new Memory<T>(array);</span> } } public void Dispose() { T[] array = this._array; if (array != null) { this._array = null; <span style='color: blue; font-weight: bold'>ArrayPool<T>.Shared.Return(array, false);</span> } } private T[] _array; } </pre> <br /> 결국 ArrayPool<T>.Shared의 구현을 기반으로 합니다. 이와 함께 또 한가지 차이점이 있다면, ArrayPool<T>와는 다르게 Shared가 아닌 별도의 Pool을 생성하는 Create 메서드를 제공하지 않는데요. 아마도 그 수요가 거의 없다고 판단했을 수 있고, 게다가 MemoryPool<T> 타입 자체가 추상 타입이기 때문에 굳이 그 정도까지 원한다면 상속을 이용하면 된다...라는 의도인 듯합니다.<br /> <br /> 참고로, IMemoryOwner<T>.Memory 속성의 타입이 전에 설명한 Memory<T>입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > C# - Span<T>와 Memory<T> ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12475'>https://www.sysnet.pe.kr/2/0/12475</a> </pre> <br /> <hr style='width: 50%' /><br /> <br /> 그럼 ETW 지원은 어떨까요? MemoryPool<T> 자체는 ETW에 대한 지원을 하고 있지 않지만, Shared 속성의 내부에서 사용하는 타입이 ArrayPool이므로 그 수준에서 ETW 이벤트가 제공됩니다. 따라서 소스 코드는 이전 글의 것과 동일하게 구현할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using System; using System.Buffers; using System.Diagnostics.Tracing; using System.Threading; namespace ConsoleApp2 { class Program { static MyEventListener _listener; // <a target='tab' href='https://www.sysnet.pe.kr/2/0/12473'>https://www.sysnet.pe.kr/2/0/12473</a> static Program() { _listener = new MyEventListener(); } static void Main(string[] args) { Console.WriteLine($"Main TID: {Thread.CurrentThread.ManagedThreadId}"); <span style='color: blue; font-weight: bold'>using var buffer = MemoryPool<byte>.Shared.Rent(4000);</span> Console.WriteLine($", BufferLen: {buffer.Memory.Length}"); } } } internal class MyEventListener : EventListener { // ...[생략: <a target='tab' href='https://www.sysnet.pe.kr/2/0/12478#etw_src'>https://www.sysnet.pe.kr/2/0/12478#etw_src</a>]... } </pre> <br /> <hr style='width: 50%' /><br /> <br /> 위에서, MemoryPool을 상속받아 사용자 정의할 수 있다고 했는데, 간단하게는 BCL에서 제공하는 구조를 그대로 베껴 다음과 같이 구현할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > public abstract class <span style='color: blue; font-weight: bold'>CMemoryPool<T> : MemoryPool<T></span> { 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); } } } } </pre> <br /> 자, 그럼 위의 코드에 부가 기능을 넣어볼까요? 그러고 보니, 이 글의 첫 번째 예제에서 MemoryPool에 반환한 버퍼가 그대로 재사용되어 이전 값이 남아 있는 것을 볼 수 있는데요, ArrayPool과는 달리 반환 시 "clearArray"를 제어할 수 없어 사용자가 직접 관련 기능을 작성해야만 합니다. 하지만, 위와 같이 코드를 만들었으니, CArrayMemoryPoolBuffer<T>.Dispose 메서드에서 반환 시 clearArray 옵션을 제어하는 값을 변경해 처리하면 개발자의 실수를 예방할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > internal class CArrayMemoryPool<T> : MemoryPool<T> { // ...[생략]... public void Dispose() { T[] array = this._array; if (array != null) { this._array = null; ArrayPool<T>.Shared.Return(array, <span style='color: blue; font-weight: bold'>true</span>); } } // ...[생략]... } </pre> <br /> 하나 더 기능을 넣어 볼까요? ^^ 가만 보니까, IMemoryOwner<T>.Memory 속성의 타입이 Memory<T>였다고 했으니 반환받은 메모리에 대한 구역을 제어하는 것이 가능합니다. ArrayPool<T>의 경우에는 T [] 타입을 반환하므로 그것이 불가능했지만, Span<T>를 담고 있는 Memory<T>라면 다음과 같이 쉽게 제어 코드를 넣을 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > private class CArrayMemoryPoolBuffer : IMemoryOwner<T>, IDisposable { <span style='color: blue; font-weight: bold'>int _size;</span> private T[] _array; public CArrayMemoryPoolBuffer(int size) { <span style='color: blue; font-weight: bold'>_size = size;</span> this._array = ArrayPool<T>.Shared.Rent(size); } public Memory<T> Memory { get { T[] array = this._array; if (array == null) { throw new NullReferenceException(); } <span style='color: blue; font-weight: bold'>return new Memory<T>(array, 0, _size);</span> } } // ...[생략]... } </pre> <br /> 따라서, 이렇게 변경한 MemoryPool 타입을 사용하면 다음과 같은 결과를 얻을 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > { IMemoryOwner<byte> buffer = CMemoryPool<byte>.Shared.Rent(1024); Console.WriteLine($"buffer[0] == {buffer.Memory.Span[0]}"); <span style='color: blue; font-weight: bold'>buffer.Memory.Span[0] = 5;</span> 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(<span style='color: blue; font-weight: bold'>1000</span>)) { Console.WriteLine(buffer.Memory.Length); } using (var buffer = CMemoryPool<byte>.Shared.Rent(<span style='color: blue; font-weight: bold'>513</span>)) { Console.WriteLine(buffer.Memory.Length); } { using var buffer = CMemoryPool<byte>.Shared.Rent(<span style='color: blue; font-weight: bold'>512</span>); Console.WriteLine(buffer.Memory.Length); } } /* 출력 결과 buffer[0] == 0 <span style='color: blue; font-weight: bold'>buffer[0] == 0 1000 513 512</span> */ </pre> <br /> 또는, 아예 Marshal.AllocHGlobal / Marshal.FreeHGlobal을 사용하여,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > C# - 고성능이 필요한 환경에서 GC가 발생하지 않는 네이티브 힙 사용 ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12036'>https://www.sysnet.pe.kr/2/0/12036</a> </pre> <br /> GC 힙을 전혀 사용하지 않는 MemoryPool을 만드는 것도 가능할 것입니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 이렇게 자유로운 커스터마이징이 가능하지만, 대개의 경우 MemoryPool보다는 ArrayPool을 사용하는 것이 좋습니다. 이와 관련해 좋은 사례가 있는데요,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > ArrayPool vs MemoryPool-minimizing allocations in AIS.NET ; <a target='tab' href='https://endjin.com/blog/2020/09/arraypool-vs-memorypool-minimizing-allocations-ais-dotnet'>https://endjin.com/blog/2020/09/arraypool-vs-memorypool-minimizing-allocations-ais-dotnet</a> </pre> <br /> 내용이 많지만, 결국 정리해 보면 MemoryPool<T>는 Rent 시 (IMemoryOwner를 구현한) 부가 개체(x64 - 24바이트의 System.Buffers.ArrayMemoryPool`1+ArrayMemoryPoolBuffer)의 사용으로 인해 GC Heap이 사용되는 문제가 있어 ArrayPool<T>로 바꿔 해결했다는 것입니다. 사실, 벤치마킹 테스트를 할 정도의 고부하에서도 MemoryPool<T> 사용 시 13MB 정도가 사용된 거라면 일반적인 응용 프로그램에서는 거의 무시해도 될 정도입니다. 단지, 해당 라이브러리가 저사양 기기에서도 사용할 수 있도록 하는 것이 목표여서 저렇게까지 할 필요가 있다고 언급하고 있습니다.<br /> <br /> 따라서, MemoryPool<T>가 부가 개체를 생성하는 반면 유연성은 있지만 ArrayPool<T>은 그 반대의 효과를 갖는다는 차이점으로 정리할 수 있습니다.<br /> <br /> 이외에 관심이 있다면 MemoryOwner<T>도 보시면 좋겠고. ^^<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > MemoryOwner<T> ; <a target='tab' href='https://learn.microsoft.com/en-us/windows/communitytoolkit/high-performance/memoryowner'>https://learn.microsoft.com/en-us/windows/communitytoolkit/high-performance/memoryowner</a> MemoryOwner<T> Class ; <a target='tab' href='https://learn.microsoft.com/en-us/dotnet/api/microsoft.toolkit.highperformance.buffers.memoryowner-1?view=win-comm-toolkit-dotnet-stable'>https://learn.microsoft.com/en-us/dotnet/api/microsoft.toolkit.highperformance.buffers.memoryowner-1?view=win-comm-toolkit-dotnet-stable</a> </pre> <br /> <hr style='width: 50%' /><br /> <a name='netfx'></a> <br /> .NET Framework의 경우 MemoryPool 타입이 기본 BCL에 없으므로,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > MemoryPool<T> Class ; <a target='tab' href='https://learn.microsoft.com/en-us/dotnet/api/system.buffers.memorypool-1'>https://learn.microsoft.com/en-us/dotnet/api/system.buffers.memorypool-1</a> 최소 요구 사항: .NET 5.0, .NET Core 2.1, .NET Standard 2.1 </pre> <br /> Nuget에서 제공하는 별도 라이브러리를 참조해야 합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > System.Memory ; <a target='tab' href='https://www.nuget.org/packages/System.Memory/'>https://www.nuget.org/packages/System.Memory/</a> </pre> (.NET Framework 4.5부터 참조 가능합니다.)<br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Install-Package System.Memory </pre> <br /> 이하 사용법은 .NET Core의 것과 동일합니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> (<a target='tab' href='https://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=1699&boardid=331301885'>첨부 파일은 이 글의 예제 코드를 포함</a>합니다.)<br /> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
2689
(왼쪽의 숫자를 입력해야 합니다.)