Как я могу заставить .NET уменьшить память

Я пытаюсь написать тест для некоторого кода, который я недавно написал, который помечает память как закрепленную, выделяя 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");
        }
    }
}

Как вы это проверяете? Использование консольного приложения или такое поведение наблюдается в рабочем приложении? Потому что для первого случая могут быть некоторые оговорки - посмотрите этот ответ

Guru Stron 02.05.2023 12:33

Я пытаюсь написать Xunit тест

mike 02.05.2023 12:34

его можно подвергнуть тем же предостережениям (теоретически). Не могли бы вы добавить минимальный воспроизводимый пример?

Guru Stron 02.05.2023 12:36

конечно, сейчас поставлю

mike 02.05.2023 12:40

если вы работаете в режиме отладки, вам нужно находиться вне функции, которая содержит ссылку, когда вы вызываете GC.Collect, иначе время жизни продлевается до конца функции. Также нужно сделать GC.Collect еще раз после звонка WaitForPendingFinalizers

Charlieface 02.05.2023 13:04

@Charlieface - звучит многообещающе. Я просто добавляю пример, иллюстрирующий проблему, но пока не учитывающий это.

mike 02.05.2023 13:07

Так что поместите строки MemUser testMemory = new MemUser(numFloats); long roughMemSize = testMemory.RoughSize(); testMemory.Clear(); в отдельную функцию

Charlieface 02.05.2023 13:10

извините за глупый вопрос, но не нужно ли мне сохранить ссылку на testMemory, чтобы перейти к функции, чтобы очистить ее? Это нормально, если эта ссылка хранится в классе Program?

mike 02.05.2023 13:12

@mike предоставленное воспроизведение подвержено точно такой же проблеме, о которой я упоминал в первом комментарии. Даже если запустить в режиме Release.

Guru Stron 02.05.2023 13:26

вам нужно поместить выделение MemUser в дополнительный метод и предотвратить встраивание этого метода, чтобы гарантировать, что никакие устаревшие ссылки стека на ваш класс не будут жить в стеке в сборках выпуска. Во время выделения адрес копируется несколько раз, и пока кадр стека жив, GC не будет освобождать память объектов, которые все еще доступны, хотя значение никогда не используется.

Alois Kraus 02.05.2023 16:20
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
10
81
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Вероятно, вам следует использовать 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, что я не совсем понимаю

mike 02.05.2023 15:56

Я создал плохой пример выше, использование GCHandle закомментировано, пока я пытаюсь выяснить поведение, когда оно не используется. На практике я делаю вызов бесплатно на выделенных дескрипторах.

mike 02.05.2023 16:22

@Майк. Поскольку вы пытаетесь вывести память, используемую процессом, вы должны понимать, что каждый строковый литерал будет интернирован автоматически. Поэтому, когда мы пишем такой пример, размер процесса увеличивается каждый раз, когда мы используем строковый литерал. Если вы воспользуетесь моим примером дословно, а затем прокомментируете вывод консоли со многими символами '=' и запустите его, вы получите совершенно другое число для использования памяти, потому что первый вызов GetTotalMemory не использует консоль или не знает никакого строкового содержимого. тем не менее, именно поэтому я поместил эту строку здесь - чтобы получить более репрезентативный результат.

majixin 02.05.2023 16:24

@mike Я бы не ожидал, что числа будут точно совпадать, вероятно, есть некоторая память, которая используется для подсчета внутренних вещей фреймворка. Если вам нужна разбивка того, для чего используется память, я бы порекомендовал использовать профилировщик памяти.

JonasH 02.05.2023 16:24

спасибо обоим, я попытаюсь написать свой тест, чтобы дать небольшую свободу действий для увеличения размера процесса во время выполнения.

mike 02.05.2023 16:27
Ответ принят как подходящий

Основная проблема заключается в том, что показанный код не вызывает 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, он был закомментирован.

mike 02.05.2023 15:50

основная разница в поведении связана с использованием GC.GetTotalMemory, у меня есть аналогичные результаты, только изменив это.

mike 02.05.2023 16:08

@Майк. Действительно. Ваш вопрос был о GCHandle, поэтому я просто показал пример, где память освобождается. В вашем реальном сценарии использования неуправляемой памяти область памяти должна быть общей и не принадлежать исключительно другому процессу. Если он принадлежит другому процессу, вместо этого вам нужно будет использовать копию. Тем не менее, если я прокомментирую dataHandle.Free() в моем примере, память не освобождается - так что это критично.

majixin 02.05.2023 16:13

Я собираюсь отметить это как ответ, так как он включает в себя GetTotalMemory, который очень помогает, а также иллюстрирует разницу, которую дает освобождение GCHandle.

mike 02.05.2023 16:42

Другие вопросы по теме