닷넷 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의 사용으로 인한 성능 향상의 벤치마크 사례가 있다면 덧글 부탁드립니다. ^^)
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]