Почему кучи больших объектов и почему нас это волнует?

106

Я читал о поколениях и куче больших объектов. Но я все еще не понимаю, в чем важность (или выгода) наличия кучи больших объектов?

Что могло бы пойти не так (с точки зрения производительности или памяти), если бы CLR просто полагалась на поколение 2 (учитывая, что порог Gen0 и Gen1 мал для обработки больших объектов) для хранения больших объектов?

Маниш Басантани
источник
6
Это вызывает у меня два вопроса для разработчиков .NET: 1. Почему дефрагментация LOH не вызывается до того, как генерируется OutOfMemoryException? 2. Почему бы объектам LOH не быть вместе (большие предпочитают конец кучи, а маленькие - в начале)
Джейкоб Брюэр,

Ответы:

195

Сборка мусора не только избавляется от объектов, на которые нет ссылок, но и сжимает кучу. Это очень важная оптимизация. Это не только делает использование памяти более эффективным (без неиспользуемых дыр), но и делает кэш ЦП намного более эффективным. Кэш - это действительно большое дело для современных процессоров, они на порядок быстрее, чем шина памяти.

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

Поэтому они провели несколько тестов, чтобы определить точку безубыточности. И мы достигли 85 000 байт в качестве точки отсечения, когда копирование больше не улучшает производительность. За специальным исключением для массивов типа double, они считаются «большими», если в массиве более 1000 элементов. Это еще одна оптимизация для 32-битного кода, распределитель кучи больших объектов имеет специальное свойство: он выделяет память по адресам, выровненным по 8, в отличие от обычного распределителя поколений, который выделяет только выровненные по 4. Это выравнивание имеет большое значение для double , чтение или запись неверно выровненного двойника очень дорого. Как ни странно, в скудной информации Microsoft никогда не упоминаются массивы длинных, непонятно, что с этим делать.

Fwiw, многие программисты обеспокоены тем, что куча больших объектов не сжимается. Это всегда срабатывает, когда они пишут программы, которые занимают более половины всего доступного адресного пространства. Затем с помощью такого инструмента, как профилировщик памяти, выяснилось, почему программа провалилась, хотя все еще оставалось много неиспользуемой виртуальной памяти. Такой инструмент показывает дыры в LOH, неиспользуемые участки памяти, где раньше находился большой объект, но собирался сборщик мусора. Такова неизбежная цена LOH, дыра может быть повторно использована только путем выделения для объекта, равного или меньшего по размеру. Настоящая проблема заключается в предположении, что программе должно быть разрешено использовать всю виртуальную память в любое время.

Проблема, которая в противном случае полностью исчезнет, ​​просто запустив код в 64-битной операционной системе. 64-битный процесс имеет 8 терабайт адресного пространства виртуальной памяти, что на 3 порядка больше, чем 32-битный процесс. Вы просто не можете выбежать из дыр.

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


ОБНОВЛЕНИЕ. .NET 4.5.1 теперь поддерживает сжатие свойства LOH, GCSettings.LargeObjectHeapCompactionMode . Остерегайтесь последствий, пожалуйста.

Ганс Пассан
источник
3
@Hans Passant, не могли бы вы прояснить ситуацию с системой x64, вы имеете в виду, что эта проблема полностью решает?
Johnny_D
Некоторые детали реализации LOH имеют смысл, но некоторые меня озадачивают. Например, я могу понять, что если создается и заброшено много больших объектов, обычно может быть желательно удалить их массово в коллекции Gen2, чем по частям в коллекциях Gen0, но если кто-то создает и отбрасывает, например, массив из 22000 строк, для которого никаких внешних ссылок не существует, какое преимущество имеет то, что коллекции Gen0 и Gen1 помечают все 22 000 строк как «живые», независимо от того, существует ли какая-либо ссылка на массив?
supercat 05
6
Конечно, проблема фрагментации такая же на x64. Для запуска вашего серверного процесса потребуется всего несколько дней, прежде чем он заработает.
Лотар
1
Хм, нет, никогда не недооценивайте 3 порядка. Сколько времени занимает сборка мусора из кучи размером 4 терабайта - это то, о чем вы не можете не узнать задолго до того, как она приблизится к этому.
Ханс Пассан
2
@HansPassant Не могли бы вы проработать это утверждение: «Сколько времени занимает сборка мусора из 4-х терабайтной кучи - это то, чего вы не можете избежать задолго до того, как оно приблизится к этому».
relative_random
9

Если размер объекта больше некоторого закрепленного значения (85000 байт в .NET 1), CLR помещает его в кучу больших объектов. Это оптимизирует:

  1. Размещение объектов (маленькие объекты не смешиваются с большими объектами)
  2. Сборка мусора (LOH собирается только при полном сборке мусора)
  3. Дефрагментация памяти (ЛОХ никогда и не редко уплотняется)
Алексей
источник
9

Существенное различие кучи малых объектов (SOH) и кучи больших объектов (LOH) заключается в том, что память в SOH сжимается при сборе, а LOH - нет, как показано в этой статье . Уплотнение больших объектов стоит больших затрат. Подобно примерам в статье, скажем, для перемещения байта в памяти требуется 2 цикла, а затем для сжатия объекта размером 8 МБ на компьютере с частотой 2 ГГц требуется 8 мс, что является большими затратами. Учитывая, что большие объекты (в большинстве случаев массивы) довольно распространены на практике, я полагаю, что это причина, по которой Microsoft закрепляет большие объекты в памяти и предлагает LOH.

Кстати, согласно этому сообщению , LOH обычно не создает проблем с фрагментами памяти.

виноград
источник
1
Загрузка больших объемов данных в управляемые объекты обычно значительно снижает затраты на 8 мс для сжатия LOH. На практике в большинстве приложений с большими данными стоимость LOH незначительна по сравнению с остальной производительностью приложения.
Shiv
3

Принцип заключается в том, что маловероятно (и, вполне возможно, плохо спроектировано), что процесс создаст множество короткоживущих больших объектов, поэтому CLR выделяет большие объекты в отдельную кучу, в которой она запускает сборщик мусора по расписанию, отличному от обычного. http://msdn.microsoft.com/en-us/magazine/cc534993.aspx

Майлз МакДоннелл
источник
Кроме того, размещение больших объектов, скажем, поколения 2 может привести к снижению производительности, поскольку для сжатия памяти потребуется много времени, особенно если будет освобождена небольшая сумма и нужно будет скопировать ОГРОМНЫЕ объекты в новое место. Текущий LOH не уплотнен по соображениям производительности.
Christopher Currens
Я думаю, что это плохой дизайн только потому, что GC плохо с этим справляется.
CodesInChaos
@CodeInChaos По-видимому, в .NET 4.5 появятся
Christian.K
1
@CodeInChaos: хотя для системы может иметь смысл подождать, пока не будет создана коллекция gen2, прежде чем пытаться освободить память даже от недолговечных объектов LOH, я не вижу никакого преимущества в производительности для объявления объектов LOH (и любых объектов, которые они содержат ссылки) безусловно живут во время коллекций gen0 и gen1. Возможны ли какие-то оптимизации при таком предположении?
supercat 03 фев.12,
@supercat Я просмотрел ссылку, упомянутую Майлзом МакДоннеллом. Насколько я понимаю: 1. Сбор LOH происходит в GC поколения 2. 2. Сборник LOH не включает уплотнение (на момент написания статьи). Вместо этого он будет отмечать мертвые объекты как повторно используемые, и эти дыры будут обслуживать будущие распределения LOH, если они будут достаточно большими. Из-за пункта 1, учитывая, что сборщик мусора поколения 2 будет медленным, если в поколении 2 много объектов, я думаю, что в этом случае лучше избегать использования LOH в максимально возможной степени.
Робби Фан,
0

Я не являюсь экспертом по CLR, но могу представить, что наличие выделенной кучи для больших объектов может предотвратить ненужную очистку сборщика мусора существующих куч поколений. Для выделения большого объекта требуется значительный объем непрерывной свободной памяти. Чтобы обеспечить это из разрозненных «дыр» в кучах поколений, вам потребуются частые уплотнения (которые выполняются только с циклами GC).

Крис Шейн
источник