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");
}
}
}