Microsoft MVP성태의 닷넷 이야기
.NET Framework: 1026. 닷넷 5에 추가된 POH (Pinned Object Heap) [링크 복사], [링크+제목 복사]
조회: 890
글쓴 사람
정성태 (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)
12687정성태6/22/202119오류 유형: 729. Invalid data: Invalid artifact, java se app service only supports .jar artifact
12686정성태6/21/202143Java: 22. Azure - 자바(Java)로 만드는 Web App Service
12685정성태6/21/202151Java: 21. Azure Web App Service에 배포된 Java 프로세스의 메모리 및 힙(Heap) 덤프 뜨는 방법
12684정성태6/19/202141오류 유형: 728. Visual Studio 2022부터 DTE.get_Properties 속성 접근 시 System.MissingMethodException 예외 발생
12683정성태6/18/202165VS.NET IDE: 166. Visual Studio 2022 - Windows Forms 프로젝트의 x86 DLL 컨트롤이 Designer에서 오류가 발생하는 문제파일 다운로드1
12682정성태6/18/202172VS.NET IDE: 165. Visual Studio 2022를 위한 Extension 마이그레이션
12681정성태6/18/202153오류 유형: 727. .NET 2.0 ~ 3.5 + x64 환경에서 System.EnterpriseServices 참조 시 CS8012 경고
12680정성태6/18/202140오류 유형: 726. python2.7.exe 실행 시 0xc000007b 오류
12679정성태6/18/202196COM 개체 관련: 23. CoInitializeSecurity의 전역 설정을 재정의하는 CoSetProxyBlanket 함수 사용법파일 다운로드1
12678정성태6/17/202176.NET Framework: 1072. C# - CoCreateInstance 관련 Inteop 오류 정리파일 다운로드1
12677정성태6/17/202175VC++: 144. 역공학을 통한 lxssmanager.dll의 ILxssSession 사용법 분석파일 다운로드1
12676정성태6/16/202192VC++: 143. ionescu007/lxss github repo에 공개된 lxssmanager.dll의 CLSID_LxssUserSession/IID_ILxssSession 사용법파일 다운로드1
12675정성태6/16/202148Java: 20. maven package 명령어 결과물로 (war가 아닌) jar 생성 방법
12674정성태6/15/202193VC++: 142. DEFINE_GUID 사용법
12673정성태6/15/202151Java: 19. IntelliJ - 자바(Java)로 만드는 Web App을 Tomcat에서 실행하는 방법
12672정성태6/15/202141오류 유형: 725. IntelliJ에서 Java webapp 실행 시 "Address localhost:1099 is already in use" 오류
12671정성태6/15/202153오류 유형: 724. Tomcat 실행 시 Failed to initialize connector [Connector[HTTP/1.1-8080]] 오류
12670정성태6/13/202169.NET Framework: 1071. DLL Surrogate를 이용한 Out-of-process COM 개체에서의 CoInitializeSecurity 문제파일 다운로드1
12669정성태6/11/2021155.NET Framework: 1070. 사용자 정의 GetHashCode 메서드 구현은 C# 9.0의 record 또는 리팩터링에 맡기세요.
12668정성태6/11/2021166.NET Framework: 1069. C# - DLL Surrogate를 이용한 Out-of-process COM 개체 제작파일 다운로드2
12667정성태6/10/202198.NET Framework: 1068. COM+ 서버 응용 프로그램을 이용해 CoInitializeSecurity 제약 해결파일 다운로드1
12666정성태6/10/202176.NET Framework: 1067. 별도 DLL에 포함된 타입을 STAThread Main 메서드에서 사용하는 경우 CoInitializeSecurity 자동 호출파일 다운로드1
12665정성태6/9/2021144.NET Framework: 1066. Wslhub.Sdk 사용으로 알아보는 CoInitializeSecurity 사용 제약파일 다운로드1
12664정성태6/9/2021101오류 유형: 723. COM+ PIA 참조 시 "This operation failed because the QueryInterface call on the COM component" 오류
12663정성태6/9/2021128.NET Framework: 1065. Windows Forms - 속성 창의 디자인 설정 지원: 문자열 목록 내에서 항목을 선택하는 TypeConverter 제작파일 다운로드1
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...