Подтвердить что ты не робот

Правильно использовать GC.Collect(); GC.WaitForPendingFinalizers();?

Я начал просматривать некоторый код в проекте и нашел что-то вроде этого:

GC.Collect();
GC.WaitForPendingFinalizers();

Эти строки обычно появляются в методах, которые предназначены для разрушения объекта под обоснованием повышения эффективности. Я сделал это замечание:

  • Вызывать сбор мусора явным образом при уничтожении каждого объекта снижает производительность, поскольку это не учитывается, если это абсолютно необходимо для производительности CLR.
  • Вызов этих инструкций в этом порядке приводит к уничтожению каждого объекта, только если завершены другие объекты. Следовательно, объект, который может быть уничтожен независимо, должен ждать другого уничтожения объекта без реальной необходимости.
  • Он может создать тупик (см.: этот вопрос)

Являются ли 1, 2 и 3 истинными? Можете ли вы дать некоторые рекомендации, подтверждающие ваши ответы?

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

4b9b3361

Ответ 1

Короткий ответ: возьмите его. Этот код почти никогда не улучшит производительность или долговременную память.

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

  • Он собирает все поколения каждый раз, а не то, что GC будет делать по умолчанию, а только собирать поколение, когда оно заполнено. Типичное использование будет собирать Gen0 (примерно) в десять раз чаще, чем Gen1, что, в свою очередь, собирает (примерно) в десять раз чаще, чем Gen2. Этот код будет собирать все поколения каждый раз. Коллекция Gen0 обычно составляет менее 100 мс; Gen2 может быть намного длиннее.
  • Он продвигает объекты, не подлежащие сборке, для следующего поколения. То есть, каждый раз, когда вы заставляете коллекцию, и у вас есть ссылка на какой-либо объект, этот объект будет продвигаться к последующему поколению. Обычно это будет происходить относительно редко, но код, такой как ниже, заставит это гораздо чаще:

    void SomeMethod()
    { 
     object o1 = new Object();
     object o2 = new Object();
    
     o1.ToString();
     GC.Collect(); // this forces o2 into Gen1, because it still referenced
     o2.ToString();
    }
    

Без GC.Collect() оба этих элемента будут собраны при следующей возможности. С коллекцией в качестве записи, o2 закончится в Gen1, что означает, что автоматическая коллекция Gen0 не выпустит эту память.

Также стоит отметить еще больший ужас: в режиме DEBUG GC функционирует по-разному и не будет возвращать любую переменную, которая все еще находится в области видимости (даже если она не используется позже в текущем методе). Таким образом, в режиме DEBUG вышеприведенный код даже не собирал o1 при вызове GC.Collect, и поэтому будут поощряться как o1, так и o2. Это может привести к некорректному и неожиданному использованию памяти при отладке кода. (Такие статьи, как this, подчеркивают это поведение.)

РЕДАКТИРОВАТЬ:. Только что протестировав это поведение, какая-то настоящая ирония: если у вас есть способ примерно так:

void CleanUp(Thing someObject)
{
    someObject.TidyUp();
    someObject = null;
    GC.Collect();
    GC.WaitForPendingFinalizers(); 
}

... тогда он явно НЕ освободит память некоторого объекта, даже в режиме RELEASE: он будет продвигать его в следующее поколение GC.

Ответ 2

Есть одна точка, которую можно сделать очень легко понять: при запуске GC автоматически очищается множество объектов за один запуск (скажем, 10000). Вызов его после каждого уничтожения очищает около одного объекта за каждый прогон.

Поскольку GC имеет высокие накладные расходы (необходимо остановить и запустить потоки, нужно сканировать все объекты вживую), предпочтительнее выполнять пакетные вызовы.

Кроме того, что может получиться после очистки после каждого объекта? Как это могло бы быть более эффективным, чем дозирование?

Ответ 3

Ваша точка номер 3 технически корректна, но может произойти только в том случае, если кто-то блокирует во время финализатора.

Даже без такого вызова блокировка внутри финализатора еще хуже, чем у вас здесь.

Есть несколько раз, когда вызов GC.Collect() действительно помогает производительности.

До сих пор я сделал это 2, может быть, 3 раза в моей карьере. (Или, может быть, около 5 или 6 раз, если вы включаете те, где я это делал, измеряли результаты, а затем вынимали их снова - и это то, что вы должны всегда измерять после выполнения).

В случаях, когда вы в течение короткого промежутка времени перебираете сотни или тысячи мегабайт памяти, а затем переключаясь на гораздо менее интенсивное использование памяти в течение длительного периода времени, оно может быть массивным или даже жизненно важное улучшение для явного сбора. Это то, что происходит здесь?

В другом месте они в лучшем случае будут делать это медленнее и использовать больше памяти.

Ответ 4

См. мой другой ответ здесь:

В GC.Collect или нет?

две вещи могут произойти, когда вы вызываете GC.Collect() самостоятельно: вы тратите больше времени на сбор коллекций (потому что обычные фоновые коллекции по-прежнему будут выполняться в дополнение к вашему ручному GC.Collect()), и вы будете висеть на память дольше (потому что вы вынудили некоторые вещи в поколение более высокого порядка, которые не нужно было туда). Другими словами, использование GC.Collect() - это почти всегда плохая идея.

О том, как вы когда-либо захотите позвонить GC.Collect(), является то, что у вас есть конкретная информация о вашей программе, которую трудно понять Мусорному коллекционеру. Канонический пример - это долговременная программа с различными циклами занятости и легкой нагрузки. Возможно, вы захотите принудительно собрать коллекцию ближе к концу периода легкой нагрузки, прежде чем цикл занят, чтобы обеспечить максимально свободный доступ к циклу занятости. Но даже здесь вы можете обнаружить, что вам лучше, переосмыслив, как ваше приложение построено (т.е. Будет ли запланированная работа работать лучше?).

Ответ 5

Я использовал это только один раз: для очистки кеша на стороне сервера документов Crystal Report. См. Мой ответ в Исключение Crystal Reports: достигнут максимальный предел заданий на обработку отчетов, настроенный вашим системным администратором

WaitForPendingFinalizers был особенно полезен для меня, так как иногда объекты не очищались должным образом. Учитывая относительно медленную производительность отчета на веб-странице - любая незначительная задержка GC была незначительной, и улучшение управления памятью дало мне более счастливый сервер.

Ответ 6

Мы столкнулись с аналогичными проблемами с @Grzenio, однако мы работаем с гораздо большими 2-мерными массивами, порядка 1000x1000-3000x3000, это в веб-сервисе.

Добавление большего объема памяти не всегда является правильным ответом, вы должны понимать свой код и прецедент. Без сбора GC мы требуем 16-32 гб памяти (в зависимости от размера клиента). Без этого нам потребуется 32-64 ГБ памяти, и даже тогда нет никаких гарантий, которые система не пострадает. Сборщик мусора .NET не идеален.

Наш веб-сервис имеет кеш в памяти порядка 5-50 миллионов строк (~ 80-140 символов на пару ключ/значение в зависимости от конфигурации), в дополнение к каждому запросу клиента мы будем строить 2 матрицы, одну из двух, один из булевых, которые затем передавались на другую службу для выполнения работы. Для 1000x1000 "матрицы" (2-мерный массив) это ~ 25 мб, за запрос. Логическое будет указывать, какие элементы нам нужны (на основе нашего кеша). Каждая запись кэша представляет собой "ячейку" в "матрице".

Производительность кеша резко ухудшается, когда сервер использует > 80% использования памяти из-за пейджинга.

Мы обнаружили, что, если мы явно не собираем сборщик мусора .net, он никогда не "очистит" переходные переменные до тех пор, пока мы не достигнем 90-95%, в результате чего производительность кэша резко снизилась.

Поскольку процесс нисходящего потока часто занимал много времени (3-900 секунд), поражение производительности коллекции GC было пренебрежимо (3-10 секунд на сбор). Мы инициировали этот сбор после, мы уже ответили клиенту.

В конечном итоге мы настроили параметры GC, а также с .net 4.6 есть дополнительные опции. Вот код .net 4.5, который мы использовали.

if (sinceLastGC.Minutes > Service.g_GCMinutes)
{
     Service.g_LastGCTime = DateTime.Now;
     var sw = Stopwatch.StartNew();
     long memBefore = System.GC.GetTotalMemory(false);
     context.Response.Flush();
     context.ApplicationInstance.CompleteRequest();
     System.GC.Collect( Service.g_GCGeneration, Service.g_GCForced ? System.GCCollectionMode.Forced : System.GCCollectionMode.Optimized);
     System.GC.WaitForPendingFinalizers();
     long memAfter = System.GC.GetTotalMemory(true);
     var elapsed = sw.ElapsedMilliseconds;
     Log.Info(string.Format("GC starts with {0} bytes, ends with {1} bytes, GC time {2} (ms)", memBefore, memAfter, elapsed));
}

После перезаписи для использования с .net 4.6 мы разделим сборщик мусора на 2 шага - собираем сбор и уплотнение.

    public static RunGC(GCParameters param = null)
    {
        lock (GCLock)
        {
            var theParams = param ?? GCParams;
            var sw = Stopwatch.StartNew();
            var timestamp = DateTime.Now;
            long memBefore = GC.GetTotalMemory(false);
            GC.Collect(theParams.Generation, theParams.Mode, theParams.Blocking, theParams.Compacting);
            GC.WaitForPendingFinalizers();
            //GC.Collect(); // may need to collect dead objects created by the finalizers
            var elapsed = sw.ElapsedMilliseconds;
            long memAfter = GC.GetTotalMemory(true);
            Log.Info($"GC starts with {memBefore} bytes, ends with {memAfter} bytes, GC time {elapsed} (ms)");

        }
    }

    // https://msdn.microsoft.com/en-us/library/system.runtime.gcsettings.largeobjectheapcompactionmode.aspx
    public static RunCompactingGC()
    {
        lock (CompactingGCLock)
        {
            var sw = Stopwatch.StartNew();
            var timestamp = DateTime.Now;
            long memBefore = GC.GetTotalMemory(false);

            GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
            GC.Collect();
            var elapsed = sw.ElapsedMilliseconds;
            long memAfter = GC.GetTotalMemory(true);
            Log.Info($"Compacting GC starts with {memBefore} bytes, ends with {memAfter} bytes, GC time {elapsed} (ms)");
        }
    }

Надеюсь, это поможет кому-то еще, поскольку мы потратили много времени на изучение этого.

Ответ 7

В целом согласитесь с ответами здесь - очень мало случаев, когда вы должны выполнять собственную сборку мусора, поскольку реализация на нативном python относительно эффективна и предсказуема. Я вижу две ситуации, когда имеет смысл взять дело в свои руки:

  1. У вас есть приложение, работающее практически в реальном времени, и сборка мусора по умолчанию запускается в неудобное время. Вы также должны спросить, почему вы в этот момент пишете чувствительный код в реальном времени на Python; а также
  2. Вы отлаживаете утечки памяти - сборка мусора непосредственно перед захватом использования памяти и поиск интересных объектов уменьшает беспорядок; Однако, если вы не используете основные функции, вам, вероятно, будет лучше использовать существующие библиотеки.