Дефрагментация памяти / сжатие кучи — обычное явление в управляемых языках, но не в C . Почему?

#c #memory-management

#c #управление памятью

Вопрос:

Я немного читал о сборщиках мусора с нулевой паузой для управляемых языков. Из того, что я понимаю, одна из самых сложных вещей, которые можно сделать без остановок в мире, — это сжатие кучи. Похоже, что только очень немногие сборщики (например, Azul C4, ZGC) делают или, по крайней мере, приближаются к этому.

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

И все же — насколько я знаю, большинство, если не все GC, все еще иногда уплотняют кучу. Я еще не видел современного GC, который не делает этого по умолчанию. Что заставляет меня поверить: Это должно быть действительно, действительно важно. Если бы это было не так, конечно, компромисс не стоил бы того.

В то же время я никогда не видел, чтобы кто-нибудь выполнял дефрагментацию памяти в C . Я уверен, что некоторые люди где-то делают, но — поправьте меня, если я ошибаюсь — это совсем не похоже на общую проблему. Я мог бы, конечно, представить, что статическая память несколько уменьшает это, но, конечно, большинство кодовых баз будут выполнять достаточное количество динамических распределений ?!

Итак, мне любопытно, почему это так?

Верны ли мои предположения (очень важные в управляемых языках; редко выполняются на C )? Если да, есть ли какое-либо объяснение, которого мне не хватает?

Комментарии:

1. Вы не можете выполнить сжатие кучи в C , потому что это изменяет адреса объектов, на которые люди сохранили указатели.

2. Это очень важно. Длительные процессы на C могут страдать и страдают от фрагментации памяти. Современные распределители памяти (такие как tcmalloc или jemalloc) пытаются предотвратить эти проблемы, используя несколько сегментов выделения для запросов одинакового размера и управляя свободным списком, чтобы сначала убедиться, что пробелы в этих сегментах заполнены, прежде чем запрашивать дополнительную память у ОС. У управляемых языков есть дополнительный стимул для этого, потому что чем меньше страниц памяти ваш GC должен сканировать на этапе маркировки, тем быстрее он выполняется.

3. C использует систему распределения памяти операционных систем (абстрагируется через new / delete и malloc / free ). И если операционная система использует виртуальную память, вы действительно не можете сжать «кучу». Причина в том, что все, что у вас есть, это виртуальные адреса, а сжатие виртуальной памяти может не перемещать данные, как вы ожидаете, в физической памяти.

4. Если вас интересуют подобные вещи, прочитайте статью jemalloc или статью mimalloc как введение в современное расширенное управление памятью.

5. Еще один метод, используемый в программах на C / C : арены памяти для кратковременных, пакетных выделений, которые можно очистить за один раз. Это также предотвращает фрагментацию памяти.

Ответ №1:

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

Однако C не может этого сделать, потому что он не знает, где находятся все указатели. Если библиотека выделения памяти перемещала вещи, могли быть висячие указатели на старые местоположения.

О, и для длительно выполняющихся процессов C действительно может страдать от фрагментации памяти. Это было большей проблемой в 32-разрядных системах, потому что это могло привести к сбою при выделении памяти из ОС, поскольку могло израсходовать все доступные блоки памяти объемом 1 МБ. В 64-разрядной версии практически невозможно создать столько сопоставлений памяти, что некуда поставить новое. Однако, если вы получили 16-байтовое выделение памяти на каждой странице памяти 4K, это много потраченного впустую пространства.

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

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

Один из методов, который некоторые старые операционные системы, такие как Apple OS 9, использовали до появления виртуальной памяти, — это дескрипторы. Вместо указателя на память выделение возвращало дескриптор. Этот дескриптор был указателем на реальный объект в памяти. Когда операционной системе требовалось сжать память или переместить ее на диск, она меняла дескриптор.

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

Комментарии:

1. Я слышал, что некоторые GC обрабатывают кучу и стек как массивы void* указателей и сканируют их на наличие указателей, которые выглядят как действительные указатели объектов на кучу или стек (диапазоны которых известны виртуальной машине) и правильно выровнены. Они также могут искать яд / помечать верхние неиспользуемые биты указателя на 64-разрядных архитектурах. Однако для такого сканирования требуется сначала остановить все потоки.

2. @MaximEgorushkin Это можно сделать таким образом. Другие системы могут выполнять трассировку из «корней», таких как стек или распределитель памяти. Ему не нужно сканировать всю память, потому что на единственно возможные допустимые указатели должен указывать один из корней. Еще один способ сделать это — хранить массивы указателей по мере их создания, а затем GC также знает, где находится каждый указатель.

3. Хорошо, имеет смысл.

Ответ №2:

Общее сжатие памяти обычно не полезно и не желательно из-за его стоимости.

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

В C можно придумать другой подход к распределению для объектов, которые вызывают фрагментацию в их конкретном приложении, например, двойные указатели или двойные индексы, позволяющие перемещать объекты; пулы объектов или арены, которые предотвращают или минимизируют фрагментацию. Такие решения для конкретных типов объектов превосходят общую сборку мусора, поскольку они используют знания, специфичные для приложения / бизнеса, что позволяет минимизировать объем / стоимость обслуживания хранилища объектов, а также выполняются в наиболее подходящее время.

Исследование показало, что языки, собирающие мусор, требуют в 5 раз больше памяти для достижения производительности программ, не эквивалентных GC. Фрагментация памяти более серьезная в языках GC.

Комментарии:

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

2. @Botje Я хотел бы увидеть более актуальные исследования.

3. Я тоже. Я немного просмотрел страницы «цитируемые» в Google Scholar и запись автора dblp, но не смог найти немедленного продолжения.