I am trying to write a test for some code I wrote recently which marks memory as pinned, by allocating a GCHandle
(for persistent use across interop with C++).
I am testing with a very large allocation in a single object that contains some float and int arrays. I want to check that once I've freed the GCHandle
for each of the arrays, the memory is able to be garbage collected.
Currently I can see the memory allocated increasing with a call to GC.GetTotalAllocatedBytes(true);
or currentProcess.WorkingSet64;
and can confirm the increase is about the same size as my rough estimate of the object being created (just adding up the size of the large arrays in the object).
However, I can't seem to get the number to shrink after removing the object, even without pinning the arrays above. I've tried
GC.Collect();
GC.WaitForPendingFinalizers();
to force collection across all generations, but my memory use never drops. I understand that the GC may be holding memory back for reuse, but I hoped there was a way to deallocate this large chunk I've created for this singular purpose..
Am I looking at the wrong metric, or is this just something that's out of my hands?
Example:
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace TestApp
{
public class MemUser
{
private float[] data;
private GCHandle dataHandle;
public float[] Data { get { return data; } set { data = value; } }
public MemUser(int numData)
{
data = new float[numData];
}
public long RoughSize()
{
return data.Length * sizeof(float);
}
public void GetPinnedHandles()
{
if (dataHandle.IsAllocated)
dataHandle.Free();
dataHandle = GCHandle.Alloc(dataHandle, GCHandleType.Pinned);
}
public void FreePinnedHandles()
{
if (dataHandle.IsAllocated)
dataHandle.Free();
}
public void Clear()
{
FreePinnedHandles();
data = null;
}
}
public class Program
{
public static void Main(string[] args)
{
const int numFloats = 500 * 1000 * 1000;
long beforeCreation = GetProcessMemory();
// Create the object using a large chunk of memory
MemUser testMemory = new MemUser(numFloats);
long roughMemSize = testMemory.RoughSize();
// in practice, will pin the memory, but testing without for now
//testMemory.GetPinnedHandles();
//confirm memory in use jumps
long afterCreation = GetProcessMemory();
long difference = afterCreation - beforeCreation;
TestResult(difference, roughMemSize);
// get rid of the object
testMemory.Clear();
testMemory = null;
// this test fails, memory may have even gone up
long afterDeletion = GetProcessMemory();
difference = afterCreation - afterDeletion;
TestResult(difference, roughMemSize);
}
public static long GetProcessMemory()
{
Process currentProcess = System.Diagnostics.Process.GetCurrentProcess();
GC.Collect();
GC.WaitForPendingFinalizers();
long totalBytesOfMemoryUsed = GC.GetTotalAllocatedBytes(true);
//long totalBytesOfMemoryUsed = currentProcess.WorkingSet64;
return totalBytesOfMemoryUsed;
}
public static void TestResult(long difference, long expected)
{
if (difference >= expected)
Console.Write($"PASS, difference ({difference}) >= {expected}\n");
else
Console.Write($"FAIL, difference ({difference}) < {expected}\n");
}
}
}
The main issue is the code shown doesn't call dataHandle.Free()
in MemUser
's Clear()
method.
You cannot rely on garbage collection to free GCHandle because it's a struct and not created on the heap.
I reworked your code to the following, which shows the memory is being cleared for the array, albeit without a few small additional number of bytes, which are likely related to string interning within the process and memory used by the console object.
As you can see if you run this, GC.Collect()
and GC.WaitForPendingFinalizers()
is not needed.
public class MemUser
{
private float[] data;
private GCHandle dataHandle;
public MemUser(int numData)
{
data = new float[numData];
dataHandle = GCHandle.Alloc(data, GCHandleType.Pinned);
}
public long RoughSize()
{
return data.Length * sizeof(float);
}
public void Clear()
{
dataHandle.Free();
data = null;
}
}
public static void Main(string[] args)
{
Console.WriteLine("====================================================================================================");
const int numFloats = 500 * 1000 * 1000;
long beforeCreation = GC.GetTotalMemory(true);
Console.WriteLine("Memory used by process: " + beforeCreation);
// Create the object using a large chunk of memory
MemUser testMemory = new MemUser(numFloats);
long afterCreation = GC.GetTotalMemory(true);
Console.WriteLine("Memory used by process after creation of large array pinned with GCHandle: " + afterCreation);
//deduce the delta
long delta = afterCreation - beforeCreation;
Console.WriteLine("Additional memory used by process for the array and GCHandle: " + delta);
GC.AddMemoryPressure(delta);
// free the memory
testMemory.Clear();
testMemory = null;
// did we reclaim all the memory? new delta should be close to zero once we free memory
long afterDeletion = GC.GetTotalMemory(true);
long newDelta = afterDeletion - beforeCreation;
Console.WriteLine("Memory after deletion: " + afterDeletion);
Console.WriteLine("Memory delta relative to before creation: " + newDelta);
Console.ReadLine();
}