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

Большие отходы кучи объектов

Я заметил, что в моем приложении заканчивается память быстрее, чем нужно. Он создает много байтовых массивов по несколько мегабайт каждый. Однако, когда я смотрел на использование памяти с помощью vmmap, появляется, что .NET выделяет гораздо больше, чем необходимо для каждого буфера. Если быть точным, при распределении буфера в 9 мегабайт,.NET создает кучу 16 мегабайт. Оставшиеся 7 мегабайт нельзя использовать для создания другого буфера в 9 мегабайт, поэтому .NET создает еще 16 мегабайт. Таким образом, каждый буфер 9 МБ отнимает 7 МБ адресного пространства!

Вот пример программы, которая генерирует исключение OutOfMemoryException после выделения 106 буферов в 32-битном .NET 4:

using System.Collections.Generic;

namespace CSharpMemoryAllocationTest
{
    class Program
    {
        static void Main(string[] args)
        {
            var buffers = new List<byte[]>();
            for (int i = 0; i < 130; ++i)
            {
                buffers.Add(new byte[9 * 1000 * 1024]);
            }

        }
    }
}

Обратите внимание, что вы можете увеличить размер массива до 16 * 1000 * 1024 и по-прежнему выделять столько же буферов, пока не исчерпаете память.

VMMap показывает это:

enter image description here

Также обратите внимание на то, что разница между общим размером управляемой кучи и общим значением Commited size составляет почти 100%. (1737MB против 946MB).

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

4b9b3361

Ответ 1

Внутри CLR выделяет память в сегментах. Из вашего описания это похоже на выделение 16 МБ сегментов, и в них выделяются массивы. Оставшееся пространство зарезервировано и не будет потрачено впустую в обычных условиях, поскольку оно будет использоваться для других распределений. Если у вас нет каких-либо распределений, которые будут вписываться в оставшиеся куски, это по существу накладные расходы.

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

Размер сегмента по умолчанию - 16 МБ, но если вы выделяете больше, чем CLR будет выделять сегменты, которые больше. Я не знаю подробностей, но, к примеру, если я выделил 20 МБ, Wmmap показывает мне сегменты 24 МБ.

Один из способов уменьшить накладные расходы - это сделать распределения, которые по возможности соответствуют размерам сегмента. Но имейте в виду, что это детали реализации и могут меняться при любом обновлении CLR.

Ответ 2

CLR резервирует 16 МБ кусок в один проход от ОС, но только активно занимает 9 МБ.

Я считаю, что вы ожидаете, что 9MB и 9MB будут в одной куче. Трудность состоит в том, что переменная теперь разделена на 2 кучи.

 Heap 1 = 9MB + 7MB
 Heap 2 = 2MB

Проблема, которую мы имеем сейчас, заключается в том, что если исходный 9MB удален, теперь у нас есть две кучи, которые мы не можем убрать, поскольку содержимое разделяется между кучами.

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

Если вы беспокоитесь об использовании памяти, не делайте этого. Использование памяти - это не плохо с .NET, так как если никто ее не использует, какая проблема? GC в какой-то момент начнет играть, и память будет убрана. GC будет пинать только в

  • Когда CLR сочтет это необходимым
  • Когда ОС сообщает CLR вернуть память
  • При принуждении с помощью кода

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

Ответ 3

Возрастный симптом алгоритма управления кучей системы приятелей, где полномочия 2 используются для рекурсивного разбиения каждого блока в двоичном дереве, поэтому для 9M следующий размер составляет 16M. Если вы уменьшите размер вашего массива до 8 мб, вы увидите, что ваше использование уменьшится наполовину. Не новая проблема, с этим справляются и отечественные программисты.

Пул небольших объектов (менее 85 000 байт) управляется по-разному, но при 9 МБ ваши массивы находятся в пуле больших объектов. Начиная с .NET 4.5, куча больших объектов не участвует в уплотнении, крупные объекты сразу же продвигаются к поколению 2.

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

Если вам нужно заполнить пространство для процесса с помощью массивов 9 МБ, выполните следующие действия:

  • Выясните, как сохранить 1 МБ для уменьшения массивов до 8 МБ сегментов.
  • Напишите или используйте сегментированный класс массивов, который абстрагирует массив из 1 или 2 МБ массивных сегментов, используя свойство indexer. Точно так же вы создаете неограниченное битовое поле или растущий ArrayList. На самом деле, я думал, что один из контейнеров уже сделал это.
  • Переместить в 64-разрядный

Восстановление фрагментированной части кучи системы приятелей - это оптимизация с логарифмической отдачей (т.е. вы все равно исчерпаете память). В какой-то момент вам придется перейти на 64-разрядную, независимо от того, удобна она или нет, если размер данных не исправлен.