I am using .NET Memory Profiler from SciTech to reduce memory allocations rate of my program and cut frequency of garbage collections.
Surprisingly, according to the profiler, the largest amount of allocations seems to be coming from GCHandle.Alloc calls I am doing to marshall existing .NET arrays to native OpenGL.
My understanding is that calling GCHandle.Alloc does not allocate memory, it only pins existing memory on the managed heap?
Am I wrong or is the profiler wrong?
.NET reference source is available for anyone to see, and you can have a look and find out for yourself.
If you dig into GCHandle.Alloc
, you'll see the it calls a native method called InternalAlloc
:
[System.Security.SecurityCritical] // auto-generated
[MethodImplAttribute(MethodImplOptions.InternalCall)]
[ResourceExposure(ResourceScope.None)]
internal static extern IntPtr InternalAlloc(Object value, GCHandleType type);
Drilling down into the CLR code, you see the internal call to MarshalNative::InternalAlloc
, which ends up calling:
hnd = GetAppDomain()->CreateTypedHandle(objRef, type);
Which in turns calls ObjectHandle::CreateTypedHandle
-> HandleTable::HndCreateHandle
-> HandleTableCache->TableAllocSingleHandleFromCache
which allocates the handle if the it doesn't exist in the cache.
As @Antosha corrected me, the place of invocation isn't via ComDelegate
(which actually makes little since) but via MarshalNative
. An allocation does occur, not on the managed heap, but an external heap reserved by the runtime for managing handle roots into GC objects. The only allocation that does occur in the managed heap is the IntPtr
which holds to pointer to the address in the table. Despite this, you should still make sure to call GCHandle.Free
once you're done.
The profiler is even assigning a specific memory amount to each GCHandle I allocate - 8 bytes. And the managed heap seems to grow 8 bytes with each GCHandle.Alloc. So it seems that it actually does allocate space on managed heap, although I have no idea what for?
I don't know how an handle could be smaller :) I've done some tests:
Console.WriteLine("Is 64 bit: {0}, IntPtr.Size: {1}", Environment.Is64BitProcess, IntPtr.Size);
int[][] objects = new int[100000][];
for (int i = 0; i < objects.Length; i++)
{
objects[i] = new int[] { 0 };
}
long w1 = Environment.WorkingSet;
GCHandle[] handles = new GCHandle[objects.Length];
for (int i = 0; i < handles.Length; i++)
{
//handles[i] = new GCHandle(handles);
//handles[i] = GCHandle.Alloc(objects[i]);
handles[i] = GCHandle.Alloc(objects[i], GCHandleType.Pinned);
}
Console.WriteLine("Allocated");
long w2 = Environment.WorkingSet;
Console.WriteLine("Used: {0}, by handle: {1}", w2 - w1, ((double)(w2 - w1)) / handles.Length);
Console.ReadKey();
It is a small program. If you run, you will see that an "empty" GCHandle
(one created with new GCHandle()
) occupies IntPtr.Size
memory. This is clear if you use ILSpy to look at it: it has a single IntPtr
field. If you pin some object, then it occupies 2*IntPtr.Size
memory. This probably because it has to write something in a CLR table (of size IntPtr
) plus its internal IntPtr
Taken from https://stackoverflow.com/a/18122621/613130
It uses a dedicated table of GC handles built inside the CLR. You allocate an entry into this table with GCHandle.Alloc() and release it again later with GCHandle.Free(). The garbage collector simply adds the entries in this table to the graph of objects it discovered itself when it performs a collection.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With