Microsoft MVP성태의 닷넷 이야기
.NET Framework: 1026. 닷넷 5에 추가된 POH (Pinned Object Heap) [링크 복사], [링크+제목 복사]
조회: 1466
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 

닷넷 5에 추가된 POH (Pinned Object Heap)

SOH(Small Object Heap)와 LOH(Large Object Heap)에 더해 .NET 5부터는 Pinned 개체만 전용으로 담는 Heap이 추가되었다고 합니다.

Internals of the POH
; https://devblogs.microsoft.com/dotnet/internals-of-the-poh/

위의 설명만 보면, POH에 어떻게 개체를 할당해야 하는지 알 수 없습니다. 사실, 그동안 알려진 방법을 보면 fixed와 GCHandle 정도가 있는데 그것들은 이미 기존 SOH/LOH에 할당된 개체를 지정해서 pinning하는 방법을 제공할 뿐입니다. 그렇다면 혹시, pinning하는 순간 POH로 복사되는 (동시에 발생하는 overhead까지도 감수하는) 걸까요? 이를 테스트하기 위해 다음과 같이 코드를 작성해 보면,

using System;
using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Sequential)]
class Program
{
    int _n = 5;

    static unsafe void Main(string[] args)
    {
        Program pg = new Program();

        {
            IntPtr ptr = GetRefAddress(pg);
            Console.WriteLine(ptr.ToInt64().ToString("x"));
        }

        // fixed로 Pinning
        fixed (int* p = &pg._n) 
        {
            IntPtr ptr = new IntPtr(p);
            Console.WriteLine(ptr.ToInt64().ToString("x"));
        }

        // GCHandle로 Pinning
        {
            GCHandle handle = GCHandle.Alloc(pg, GCHandleType.Pinned);

            Console.WriteLine(handle.AddrOfPinnedObject().ToInt64().ToString("x"));
            Console.WriteLine(handle);
        }
    }

    private unsafe static IntPtr GetRefAddress(object obj)
    {
        TypedReference refA = __makeref(obj);
        return **(IntPtr**)&refA;
    }
}

/* 출력결과
297314cb578
297314cb580
297314cb580
*/

pinning으로 인한 주소가 크게 벗어나지 않는 걸로 봐서 별도의 POH로 이동한 것 같지는 않습니다. 다시 말해, 이것은 POH를 추가했다고 해서 기존 작성한 코드에 어떤 영향이 있는 것은 아님을 의미합니다.




사용 방법을 찾기 위해 검색했더니 POH에 대해 더 실질적으로 설명하는 글이 나옵니다. ^^

Pinned Object Heap in .NET 5
; https://tooslowexception.com/pinned-object-heap-in-net-5/

Pinned Heap
; https://github.com/dotnet/coreclr/blob/master/Documentation/design-docs/PinnedHeap.md

아하... POH 힙을 활용하기 위해 .NET 5부터 새롭게 GC.AllocateArray 메서드를 제공하고 있군요.

GC.AllocateArray(Int32, Boolean) Method
; https://docs.microsoft.com/en-us/dotnet/api/system.gc.allocatearray

내부 구현을 보면,

// C:\Program Files\dotnet\shared\Microsoft.NETCore.App\5.0.3\System.Private.CoreLib.dll
// System.GC
public static T[] AllocateArray<[Nullable(2)] T>(int length, bool pinned = false)
{
    GC.GC_ALLOC_FLAGS flags = GC.GC_ALLOC_FLAGS.GC_ALLOC_NO_FLAGS;
    if (pinned)
    {
        if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
        {
            ThrowHelper.ThrowInvalidTypeWithPointersNotSupported(typeof(T));
        }
        flags = GC.GC_ALLOC_FLAGS.GC_ALLOC_PINNED_OBJECT_HEAP;
    }
    return Unsafe.As<T[]>(GC.AllocateNewArray(typeof(T[]).TypeHandle.Value, length, flags));
}

[MethodImpl(MethodImplOptions.InternalCall)]
internal static extern Array AllocateNewArray(IntPtr typeHandle, int length, GC.GC_ALLOC_FLAGS flags);

pinned == true인 경우 RuntimeHelpers.IsReferenceOrContainsReferences 메서드를 호출해 검사하고 있는데요,

RuntimeHelpers.IsReferenceOrContainsReferences Method
; https://docs.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.runtimehelpers.isreferenceorcontainsreferences

메서드의 이름에서도 유추할 수 있지만 순수 blittable 타입인지를 판단하는 역할을 합니다. 그러고 보니 지난 글에서,

C# - GC 힙이 아닌 Native 힙에 인스턴스 생성 - 0SuperComicLib.LowLevel 라이브러리 소개
; https://www.sysnet.pe.kr/2/0/12538

NativeClass.InitObj로 할당할 수 있는 개체의 조건으로 제네릭의 unmanaged 제약을 만족할 수 있어야 한다고 했는데, 바로 그 조건을 테스트할 수 있는 방법을 (.NET Core 2.0부터) 메서드로도 제공하고 있었던 것입니다.

그리고, GC.AllocateArray도 역시 (NativeClass.InitObj와 유사하게) pinning 해야 하는 개체라면 blittable 타입에 한해서 허용한다는 공통점이 있습니다.




POH에 개체를 할당하는 방법은 설명했고, 그렇다면 그게 어떤 의미가 있을까요? 이에 대해서는 "Internals of the POH" 글에서 이미 잘 설명하고 있습니다.

기존에도 개체를 fixed와 GCHandle로 pinning을 했는데, 그중에서 fixed의 경우에는 지정된 block이 확실하므로 보통은 짧게 pin/unpin이 되어 GC 구동 시 크게 부담이 없었습니다. 반면 GCHandle로 pinning하는 경우에는 GCHandle.Free를 하기 전까지는 메모리 고정이 해제되지 않으므로 장시간 SOH/LOH에 점유될 수 있고 특정 조건에서 GC의 메모리 축소(compacting)를 방해해 힙의 파편화를 증가시키며 GC 효율을 낮추게 됩니다.

그런데, 따지고 보면 일반 개발자 입장에서 - 이 글을 읽고 있는 여러분 중에 GCHandle 사용을 얼마나 사용해 보셨는지 묻고 싶군요. ^^ 아마 거의 사용해 본 적이 없을 것이므로 POH가 추가되었다고 해서 뭔가 극적인 성능 향상을 기대할 수 있는 여지가 많지 않습니다. 물론, Win32 API 등을 자주 호출한다면 pinning을 CLR 내부에서 자주 하겠지만 엄밀히 그 정도는 fixed와 유사하게 단기적으로만 점유하는 것에 불과하므로 마찬가지로 GC를 크게 방해하지는 않습니다.

그럼에도 불구하고, 이것이 유용한 사례가 있습니다. "Pinned Object Heap in .NET 5" 글의 작성자는 마지막에 ArrayPool의 버퍼로 사용할 것이라고 마무리하고 있습니다. 또한 이와 유사하게 Kestrel의 MemoryPool에 POH를 사용한 것이 POH의 사용 예라고 언급하고 있습니다.

정리해 보면, 그동안 pinning을 한 번도 사용해 본 적이 없는 분이라면 그냥 자신이 사용하고 있는 하부 framework에서의 성능 향상을 기대하면서 기존처럼 프로그램하시면 되겠습니다. 여기에 개인적인 의견을 덧붙이면, 근래 들어 C# 언어에서도 나타나는 경향이지만 아마도 이것은 마이크로소프트 내부에서 어떻게든지 ASP.NET Core의 벤치마크 수치를 좀 더 높이기 위한 마이크로 튜닝의 산물이 아닌가 생각됩니다. ^^

(혹시 POH의 사용으로 인한 성능 향상의 벤치마크 사례가 있다면 덧글 부탁드립니다. ^^)




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

[연관 글]


donaricano-btn



[최초 등록일: ]
[최종 수정일: 3/28/2021]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 쓴 사람
 



2021-03-02 10시30분
[dimohy] 와우 그렇군요. 제가 GCHandle을 쓸일이 없어서 전반적인 이해가 부족했는데, 설명이 이해에 도움이 되었습니다. 시간내셔서 검증하여 감사드리고, 결론과 신빙성 있는 의견(말씀으로 보아 맞는 것 같습니다)도 재밌었습니다.
[손님]
2021-03-03 09시34분
[노말개발자] 안녕하세요. 먼발치에서 응원을 해오다 아래 내용에서 질문이 있어서 첫글을 남겨봅니다.
제가 작성중인 프로그램에는 특정 함수에서 반복적(30fps * x)으로 Intptr로 넘겨야 하는 구조가 있어
GCHandle.Alloc , Free를 반복적으로 하거나 Alloc을 한 후 특정 이벤트가 있을경우에 Free를 하는 2가지 구조중 후자를 선택했는데요.
아래 내용을 보면 pinning을 오래 점유하게 되면 GC의 효율을 낮출수 있다고 하셨는데,
이부분에서 1: 반복적으로 alloc과, free vs 2. pinning 장기간 점유 둘중에 어떤 부분이 성능에 더 효율적일지 확인해 볼 수 있는 방법이 있을까요?

// 설명 내용
기존에도 개체를 fixed와 GCHandle로 pinning을 했는데, 그중에서 fixed의 경우에는 지정된 block이 확실하므로 보통은 짧게 pin/unpin이 되어 GC 구동 시 크게 부담이 없었습니다. 반면 GCHandle로 pinning하는 경우에는 GCHandle.Free를 하기 전까지는 메모리 고정이 해제되지 않으므로 장시간 SOH/LOH에 점유될 수 있고 특정 조건에서 GC의 메모리 축소(compacting)를 방해해 힙의 파편화를 증가시키며 GC 효율을 낮추게 됩니다.
[손님]
2021-03-03 01시22분
@노말개발자 일단 "Internals of the POH" 문서의 "The worst scenario is ..."글에 따르면 Alloc/Free를 짧게 반복적으로 하는 것이 더 낫습니다. 특정 이벤트에 따라 Free를 하면 그 사이에 다른 개체들이 생성되고 그 와중에 GC가 발생하면 pinning 시킨 개체들의 사이에 발생한 free 영역이 발생하게 되고 이를 고려한 오버헤드가 있게 됩니다.

하지만, 이것에 대해 어떤 방법이 더 성능에 효율적인지 확인할 수 있는 직접적인 수치는 없습니다.

개인적인 의견으로도, Alloc과 Free 사이의 기간, 그 사이 발생하는 개체 할당과 GC의 횟수가 응용 프로그램 별로 다양할 수 있고 Alloc/Free 자체의 오버헤드가 어느 정도인지도 감안을 해야 하기 때문에, 자신의 응용 프로그램에서 효율을 따지기 위해서는 직접 성능 측정을 하는 것이 최선으로 보입니다.
정성태
2021-03-04 09시45분
[노말개발자] @정성태 감사합니다. 시간이 되면 성능 분석을 해봐야겠네요.
[손님]
2021-03-13 02시50분
[슈퍼코믹] 역시 닷넷 개발자분들도 reference 까지 고정시킬순 없었나봅니다ㅎㅎ
그래도 POH가 .net 5에 추가되었다니 어쨌거나 뭔가 새롭네요!
제가 뭔가 영향을 준게 있었으면 참 좋겠는데 말이죠~
.net 5가 대세가 되면 저도 Native Heap대신 POH를 사용하는 방향으로 틀어야겠습니다...
그런데 POH가 Marshal이 아니라 GC에 있다는건 GC가 관리한다는 것이겠죠?
메모리를 원하는 타이밍에 수거하도록 배려하진 않은 모양이네요
[손님]
2021-03-13 11시46분
근데, 사실 NativeClass와 POH는 그 성격이 많이 다릅니다. POH는 GC 개체들 사이에서 Pinning된 개체들 때문에 GC 효율이 떨어지는 것을 방지하기 위해 만들어진 것이기 때문에 원하는 시간에 메모리를 수거할 필요는 없습니다.

또한 reference를 고정시키지 않은 이유는 못한 것이 아니고 효율을 위해 하지 않은 것입니다. 현실적으로 Pinning 개체들은 대체로 blittable 타입이고 실제로 닷넷 BCL에서 제공하는 GC.AllocateArray는 그것들의 배열 (역시 이것도 현실적으로는 byte 배열)이기 때문에 reference까지 포함할 이유는 없었던 것입니다. 구현면에서 POH는 LOH와 유사하게 2세대 GC에서 수집이 되고 Pinning 개체를 담고 있다는 것만 특별할 뿐 그 외에는 LOH와 다른 면이 없으므로 reference를 담지 못할 기술적인 이유는 없습니다.
정성태
2021-03-13 06시12분
[슈퍼코믹] 제가 오해를 불러일으킬만한 단어를 사용했었나 봅니다.
기술적으로 불가능하다고 생각하진 않습니다. NativeClass에 reference type을 pinned시키려고 해본 경험이 있으므로 닷넷 개발자들이 reference를 pinning하지 못하도록 한 것이 기술적으로 구현할 수가 없어서가 아닌 효율에 문제라는 것을 이전부터 알고있었습니다.

또한, 정성태님 말씀처럼 POH는 기존 pinned object들이 그렇지 않은 object 사이에 있어서 GC의 효율을 떨어뜨리는 문제를 해결해주는데에 의미가 있는 것이 맞습니다.
실제로 pinned 코드를 만드는 fixed (T* p = &v)일때 T가 blittable 타입일 가능성이 높기도 하고요, 물론 IL에서 pinned object 변수를 선언해버리면 reference type에 대해서 pinned하게 되지만 ㅎㅎ
[손님]

[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
12849정성태10/17/2021169스크립트: 33. JavaScript와 C#의 시간 변환
12848정성태10/17/2021108스크립트: 32. 파이썬 - sqlite3 기본 예제 코드
12847정성태10/14/202177스크립트: 31. 파이썬 gunicorn - WORKER TIMEOUT 오류 발생
12846정성태10/7/2021221스크립트: 30. 파이썬 __debug__ 플래그 변수에 따른 코드 실행 제어
12845정성태10/6/2021415.NET Framework: 1120. C# - BufferBlock<T> 사용 예제 [2]파일 다운로드1
12844정성태10/3/2021199오류 유형: 764. MSI 설치 시 "... is accessible and not read-only." 오류 메시지
12843정성태10/3/2021209스크립트: 29. 파이썬 - fork 시 기존 클라이언트 소켓 및 스레드의 동작파일 다운로드1
12842정성태10/1/2021209오류 유형: 763. 파이썬 오류 - AttributeError: type object '...' has no attribute '...'
12841정성태10/1/2021273스크립트: 28. 모든 파이썬 프로세스에 올라오는 특별한 파일 - sitecustomize.py
12840정성태9/30/2021301.NET Framework: 1119. Entity Framework의 Join 사용 시 다중 칼럼에 대한 OR 조건 쿼리파일 다운로드1
12839정성태9/15/2021517.NET Framework: 1118. C# 10 - (17) 제네릭 타입의 특성 적용파일 다운로드1
12838정성태9/13/2021503.NET Framework: 1117. C# - Task에 전달한 Action, Func 유형에 따라 달라지는 async/await 비동기 처리 [2]파일 다운로드1
12837정성태9/11/2021287VC++: 151. Golang - fmt.Errorf, errors.Is, errors.As 설명
12836정성태9/10/2021279Linux: 45. 리눅스 - 실행 중인 다른 프로그램의 출력을 확인하는 방법
12835정성태9/7/2021291.NET Framework: 1116. C# 10 - (16) CallerArgumentExpression 특성 추가파일 다운로드1
12834정성태9/7/2021243오류 유형: 762. Visual Studio 2019 Build Tools - 'C:\Program' is not recognized as an internal or external command, operable program or batch file.
12833정성태9/6/2021361VC++: 150. Golang - TCP client/server echo 예제 코드파일 다운로드1
12832정성태9/6/2021238VC++: 149. Golang - 인터페이스 포인터가 의미 있을까요?
12831정성태9/6/2021241VC++: 148. Golang - 채널에 따른 다중 작업 처리파일 다운로드1
12830정성태9/6/2021241오류 유형: 761. Internet Explorer에서 파일 다운로드 시 "Your current security settings do not allow this file to be downloaded." 오류
12829정성태9/5/2021329.NET Framework: 1115. C# 10 - (15) 구조체 타입에 기본 생성자 정의 가능파일 다운로드1
12828정성태9/4/2021310.NET Framework: 1114. C# 10 - (14) 단일 파일 내에 적용되는 namespace 선언파일 다운로드1
12827정성태9/4/2021237스크립트: 27. 파이썬 - 웹 페이지 데이터 수집을 위한 scrapy Crawler 사용법 요약
12826정성태9/3/2021322.NET Framework: 1113. C# 10 - (13) 문자열 보간 성능 개선파일 다운로드1
12825정성태9/3/2021223개발 환경 구성: 603. GoLand - WSL 환경과 연동
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...