Microsoft MVP성태의 닷넷 이야기
.NET Framework: 1026. 닷넷 5에 추가된 POH (Pinned Object Heap) [링크 복사], [링크+제목 복사]
조회: 11032
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 
(연관된 글이 2개 있습니다.)
(시리즈 글이 9개 있습니다.)
.NET Framework: 497. .NET Garbage Collection에 대한 정리
; https://www.sysnet.pe.kr/2/0/1862

.NET Framework: 728. windbg - 눈으로 확인하는 Workstation GC / Server GC
; https://www.sysnet.pe.kr/2/0/11445

.NET Framework: 729. windbg로 살펴보는 GC heap의 Segment 구조
; https://www.sysnet.pe.kr/2/0/11446

.NET Framework: 1026. 닷넷 5에 추가된 POH (Pinned Object Heap)
; https://www.sysnet.pe.kr/2/0/12545

.NET Framework: 1029. C# - GC 호출로 인한 메모리 압축(Compaction)을 확인하는 방법
; https://www.sysnet.pe.kr/2/0/12572

.NET Framework: 1059. 세대 별 GC(Garbage Collection) 방식에서 Card table의 사용 의미
; https://www.sysnet.pe.kr/2/0/12649

.NET Framework: 1060. 닷넷 GC에 새롭게 구현되는 DPAD(Dynamic Promotion And Demotion for GC)
; https://www.sysnet.pe.kr/2/0/12653

.NET Framework: 2024. .NET 7에 도입된 GC의 메모리 해제에 대한 segment와 region의 차이점
; https://www.sysnet.pe.kr/2/0/13083

닷넷: 2209. .NET 8 - NonGC Heap / FOH (Frozen Object Heap)
; https://www.sysnet.pe.kr/2/0/13536




닷넷 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://learn.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://learn.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의 사용으로 인한 성능 향상의 벤치마크 사례가 있다면 덧글 부탁드립니다. ^^)




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 12/3/2023]

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

비밀번호

댓글 작성자
 



2021-03-02 10시30분
[dimohy] 와우 그렇군요. 제가 GCHandle을 쓸일이 없어서 전반적인 이해가 부족했는데, 설명이 이해에 도움이 되었습니다. 시간내셔서 검증하여 감사드리고, 결론과 신빙성 있는 의견(말씀으로 보아 맞는 것 같습니다)도 재밌었습니다.
[guest]
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 효율을 낮추게 됩니다.
[guest]
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분
[노말개발자] @정성태 감사합니다. 시간이 되면 성능 분석을 해봐야겠네요.
[guest]
2021-03-13 02시50분
[슈퍼코믹] 역시 닷넷 개발자분들도 reference 까지 고정시킬순 없었나봅니다ㅎㅎ
그래도 POH가 .net 5에 추가되었다니 어쨌거나 뭔가 새롭네요!
제가 뭔가 영향을 준게 있었으면 참 좋겠는데 말이죠~
.net 5가 대세가 되면 저도 Native Heap대신 POH를 사용하는 방향으로 틀어야겠습니다...
그런데 POH가 Marshal이 아니라 GC에 있다는건 GC가 관리한다는 것이겠죠?
메모리를 원하는 타이밍에 수거하도록 배려하진 않은 모양이네요
[guest]
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하게 되지만 ㅎㅎ
[guest]
2021-11-07 10시55분
POH가 pinning 개체를 담고 있다고 표현하셨는데. POH에서는 압축이 일어나지 않기 때문에 그렇게 표현되는 걸까요? 아니면 POH에 할당되는 메모리에 한해서는 내부적으로 Pinning이라는 특정 동작이 일어나고 있는 것일까요? 전자 일거라고 생각되지만 내부 동작에 대한 언급이 참조 걸어주신 포스팅에서도 잘 보이지 않아 생각을 여쭙습니다 ^^
bluezc
2021-11-08 09시18분
POH가 새롭게 나온 것에 상관없이, Pinning 개체 자체가 이미 압축할 수 없는 (이동해서는 안 되는) 메모리 영역입니다. 답이 되었을까요?
정성태
2021-11-08 09시40분
답변 감사드립니다. 많은 도움이 되었습니다.
bluezc

... 16  17  18  19  20  21  22  23  24  25  26  [27]  28  29  30  ...
NoWriterDateCnt.TitleFile(s)
12946정성태1/28/20226025오류 유형: 792. .NET Core - 로컬 개발 중에 docker 호스팅으로 바꾸는 경우 SQL 서버 접근 방법
12945정성태1/28/20226263오류 유형: 791. SQL 서버 로그인 시 localhost는 되고, 127.0.0.1로는 안 되는 문제
12944정성태1/28/20228629.NET Framework: 1143. C# - Entity Framework Core 6 개요
12943정성태1/27/20227540.NET Framework: 1142. .NET 5+로 포팅 시 플랫폼 호환성 경고 메시지(SYSLIB0006, SYSLIB0011, CA1416)파일 다운로드1
12942정성태1/27/20227811.NET Framework: 1141. XmlSerializer와 Dictionary 타입파일 다운로드1
12941정성태1/26/20229216오류 유형: 790. AKS/k8s - pod 상태가 Pending으로 지속되는 경우
12940정성태1/26/20226637오류 유형: 789. AKS에서 hpa에 따른 autoscale 기능이 동작하지 않는다면?
12939정성태1/25/20227314.NET Framework: 1140. C# - ffmpeg(FFmpeg.AutoGen)를 이용해 MP3 오디오 파일 인코딩/디코딩하는 예제파일 다운로드1
12938정성태1/24/20229586개발 환경 구성: 633. Docker Desktop + k8s 환경에서 local 이미지를 사용하는 방법
12937정성태1/24/20227424.NET Framework: 1139. C# - ffmpeg(FFmpeg.AutoGen)를 이용해 오디오(mp2) 인코딩하는 예제(encode_audio.c) [2]파일 다운로드1
12936정성태1/22/20227381.NET Framework: 1138. C# - ffmpeg(FFmpeg.AutoGen)를 이용해 멀티미디어 파일의 메타데이터를 보여주는 예제(metadata.c)파일 다운로드1
12935정성태1/22/20227560.NET Framework: 1137. ffmpeg의 파일 해시 예제(ffhash.c)를 C#으로 포팅파일 다운로드1
12934정성태1/22/20227117오류 유형: 788. Warning C6262 Function uses '65564' bytes of stack: exceeds /analyze:stacksize '16384'. Consider moving some data to heap. [2]
12933정성태1/21/20227666.NET Framework: 1136. C# - ffmpeg(FFmpeg.AutoGen)를 이용해 MP2 오디오 파일 디코딩 예제(decode_audio.c)파일 다운로드1
12932정성태1/20/20228123.NET Framework: 1135. C# - ffmpeg(FFmpeg.AutoGen)로 하드웨어 가속기를 이용한 비디오 디코딩 예제(hw_decode.c) [2]파일 다운로드1
12931정성태1/20/20226283개발 환경 구성: 632. ASP.NET Core 프로젝트를 AKS/k8s에 올리는 과정
12930정성태1/19/20226889개발 환경 구성: 631. AKS/k8s의 Volume에 파일 복사하는 방법
12929정성태1/19/20226669개발 환경 구성: 630. AKS/k8s의 Pod에 Volume 연결하는 방법
12928정성태1/18/20226818개발 환경 구성: 629. AKS/Kubernetes에서 호스팅 중인 pod에 shell(/bin/bash)로 진입하는 방법
12927정성태1/18/20226556개발 환경 구성: 628. AKS 환경에 응용 프로그램 배포 방법
12926정성태1/17/20227040오류 유형: 787. AKS - pod 배포 시 ErrImagePull/ImagePullBackOff 오류
12925정성태1/17/20227154개발 환경 구성: 627. AKS의 준비 단계 - ACR(Azure Container Registry)에 docker 이미지 배포
12924정성태1/15/20228625.NET Framework: 1134. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 비디오 디코딩 예제(decode_video.c) [2]파일 다운로드1
12923정성태1/15/20227588개발 환경 구성: 626. ffmpeg.exe를 사용해 비디오 파일을 MPEG1 포맷으로 변경하는 방법
12922정성태1/14/20226639개발 환경 구성: 625. AKS - Azure Kubernetes Service 생성 및 SLO/SLA 변경 방법
12921정성태1/14/20225626개발 환경 구성: 624. Docker Desktop에서 별도 서버에 설치한 docker registry에 이미지 올리는 방법
... 16  17  18  19  20  21  22  23  24  25  26  [27]  28  29  30  ...