Я пытаюсь написать тест для некоторого кода, который я недавно написал, который помечает память как закрепленную, выделяя GCHandle (для постоянного использования при взаимодействии с C++).
Я тестирую с очень большим выделением в одном объекте, который содержит несколько массивов с плавающей запятой и int. Я хочу проверить, что после того, как я освободил GCHandle для каждого из массивов, память может быть удалена сборщиком мусора.
В настоящее время я вижу, что объем выделенной памяти увеличивается при вызове GC.GetTotalAllocatedBytes(true); или currentProcess.WorkingSet64;, и могу подтвердить, что увеличение примерно соответствует моей приблизительной оценке создаваемого объекта (просто суммируя размер больших массивов в объекте).
Однако я не могу заставить число уменьшиться после удаления объекта, даже без закрепления массивов выше. я пробовал
GC.Collect();
GC.WaitForPendingFinalizers();
для принудительного сбора всех поколений, но использование моей памяти никогда не снижается. Я понимаю, что GC может удерживать память для повторного использования, но я надеялся, что есть способ освободить этот большой кусок, который я создал для этой единственной цели.
Я смотрю на неправильную метрику, или это просто что-то, что не в моих руках?
Пример:
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");
}
}
}
Я пытаюсь написать Xunit тест
его можно подвергнуть тем же предостережениям (теоретически). Не могли бы вы добавить минимальный воспроизводимый пример?
конечно, сейчас поставлю
если вы работаете в режиме отладки, вам нужно находиться вне функции, которая содержит ссылку, когда вы вызываете GC.Collect, иначе время жизни продлевается до конца функции. Также нужно сделать GC.Collect еще раз после звонка WaitForPendingFinalizers
@Charlieface - звучит многообещающе. Я просто добавляю пример, иллюстрирующий проблему, но пока не учитывающий это.
Так что поместите строки MemUser testMemory = new MemUser(numFloats); long roughMemSize = testMemory.RoughSize(); testMemory.Clear(); в отдельную функцию
извините за глупый вопрос, но не нужно ли мне сохранить ссылку на testMemory, чтобы перейти к функции, чтобы очистить ее? Это нормально, если эта ссылка хранится в классе Program?
@mike предоставленное воспроизведение подвержено точно такой же проблеме, о которой я упоминал в первом комментарии. Даже если запустить в режиме Release.
вам нужно поместить выделение MemUser в дополнительный метод и предотвратить встраивание этого метода, чтобы гарантировать, что никакие устаревшие ссылки стека на ваш класс не будут жить в стеке в сборках выпуска. Во время выделения адрес копируется несколько раз, и пока кадр стека жив, GC не будет освобождать память объектов, которые все еще доступны, хотя значение никогда не используется.





Вероятно, вам следует использовать GC.GetTotalMemory(true) , чтобы получить текущее использование памяти. См. также Как узнать количество памяти, используемой приложением.
GC.GetTotalAllocatedBytes(true);
Из документации это выглядит так, как будто подсчитывается общее количество выделений, а не текущая память.
текущийПроцесс.РабочийНабор64;
Поскольку это память процесса, я бы предположил, что она включает в себя память, которая управляется сборщиком мусора, но в настоящее время не используется. Когда память будет собрана, сборщик мусора не сразу вернет ее в ОС, так как она, вероятно, скоро понадобится снова.
Но есть и другие проблемы. При использовании GCHandle.Alloc вы добровольно отказываетесь от сбора мусора. Поэтому очень важно, чтобы каждый вызов alloc соответствовал вызову free, по крайней мере, для любого продолжительного процесса.
Я бы, наверное, порекомендовал использовать фиксированный оператор , если это возможно. Это должно быть и безопаснее, и позволять перемещать память, когда она не используется активно нативным кодом. Длительное закрепление небольших объектов может привести к фрагментации памяти.
использование этого дает мне более привлекательные различия, очень близкие к тому, что я ожидаю, но по какой-то причине разница немного меньше, чем ожидалось после очистки объекта (поясняется ниже как likely related to string interning within the process and memory used by the console object, что я не совсем понимаю
Я создал плохой пример выше, использование GCHandle закомментировано, пока я пытаюсь выяснить поведение, когда оно не используется. На практике я делаю вызов бесплатно на выделенных дескрипторах.
@Майк. Поскольку вы пытаетесь вывести память, используемую процессом, вы должны понимать, что каждый строковый литерал будет интернирован автоматически. Поэтому, когда мы пишем такой пример, размер процесса увеличивается каждый раз, когда мы используем строковый литерал. Если вы воспользуетесь моим примером дословно, а затем прокомментируете вывод консоли со многими символами '=' и запустите его, вы получите совершенно другое число для использования памяти, потому что первый вызов GetTotalMemory не использует консоль или не знает никакого строкового содержимого. тем не менее, именно поэтому я поместил эту строку здесь - чтобы получить более репрезентативный результат.
@mike Я бы не ожидал, что числа будут точно совпадать, вероятно, есть некоторая память, которая используется для подсчета внутренних вещей фреймворка. Если вам нужна разбивка того, для чего используется память, я бы порекомендовал использовать профилировщик памяти.
спасибо обоим, я попытаюсь написать свой тест, чтобы дать небольшую свободу действий для увеличения размера процесса во время выполнения.
Основная проблема заключается в том, что показанный код не вызывает dataHandle.Free() в MemUser методе Clear().
Вы не можете полагаться на сборку мусора для освобождения GCHandle, потому что это структура, а не созданная в куче.
Я переработал ваш код следующим образом, который показывает, что память очищается для массива, хотя и без небольшого дополнительного количества байтов, которые, вероятно, связаны с интернированием строк внутри процесса и памятью, используемой объектом консоли.
Как вы можете видеть, если вы запустите это, GC.Collect() и GC.WaitForPendingFinalizers() не нужны.
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();
}
вы правы, этот пример кода никогда не вызывал dataHandle.Free, но он также никогда не вызывал GetPinnedHandles, он был закомментирован.
основная разница в поведении связана с использованием GC.GetTotalMemory, у меня есть аналогичные результаты, только изменив это.
@Майк. Действительно. Ваш вопрос был о GCHandle, поэтому я просто показал пример, где память освобождается. В вашем реальном сценарии использования неуправляемой памяти область памяти должна быть общей и не принадлежать исключительно другому процессу. Если он принадлежит другому процессу, вместо этого вам нужно будет использовать копию. Тем не менее, если я прокомментирую dataHandle.Free() в моем примере, память не освобождается - так что это критично.
Я собираюсь отметить это как ответ, так как он включает в себя GetTotalMemory, который очень помогает, а также иллюстрирует разницу, которую дает освобождение GCHandle.
Как вы это проверяете? Использование консольного приложения или такое поведение наблюдается в рабочем приложении? Потому что для первого случая могут быть некоторые оговорки - посмотрите этот ответ