Имеет ли управление памятью Rust во фрагментированной памяти?

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

Я понимаю, что его система типов (типы собственности, заимствования, Rc, Arc) позволяет ей детерминистически знать во время компиляции, когда освобожденный фрагмент выделенной памяти может быть освобожден.

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

Ответ 1

TL; DR: большинству программ никогда не придется беспокоиться о фрагментации в C, С++ или Rust. Те, кому это нужно, придется обрабатывать сами.


Требуется ли автоматическому управлению памятью языка программирования ржавчины восстановить фрагментированную память?

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

В целом, язык должен иметь Compact GC для компактной фрагментированной памяти. Rust, как и C и С++, не имеет GC, поэтому его память может быть фрагментирована в зависимости от использования и не может быть дефрагментирована без программы, освобождающей раздражающие блоки, потому что перемещение невозможно.


Однако, прежде чем мы начнем страдать от фрагментации, мы должны сначала подумать о том, что это значит.

Каков эффект фрагментации?

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

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

Действительно, он отмечает, что фрагментация не является единственным источником отходов; чрезмерное распределение также является общей проблемой:

  • a Vec будет выделять количество элементов из 2-х элементов, но, возможно, вы используете только 2^N + 1, тратите 2^N - 1 слоты
  • a BTreeMap или HashMap выделяют больше места, чем они действительно используют
  • даже распределитель памяти обычно выделяет куски предопределенных размеров, поэтому запрос на 157 байтов может быть округлен до, возможно, до 196 байт, теряя 39 байт.

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

Это подчеркивает очень важный факт: мало смысла избавляться от фрагментации, если вы потребляете столько памяти из-за бухгалтерского учета/накладных расходов, ваша схема распределения налагает на вас те же проблемы.


Как современные распределители управляют памятью?

Современные распределители НЕ являются вашими распределителями бесплатного списка.

Типичная схема распределения относительно проста и при этом очень хороша при сохранении фрагментации для небольших запросов:

  • Большие слябы памяти для "больших" запросов (близкие или превышающие размер страницы ОС: 4kB в целом)
  • Небольшие слябы памяти для "меньших" запросов

Для небольших плит количество классов определяется размером. Например: (0, 8], (8, 12], (12, 16],..., (164, 196],..., (..., 512]. Каждый размер класса управляет собственным списком страниц ОС и вырезает каждую страницу ОС для собственного использования. Примером класса 512 байтов на странице ОС на 4 КБ может быть:

+---+---+---+---+---+---+---+---+
| a | b | c | d | e | f | g | h |
+---+---+---+---+---+---+---+---+

где для распределения доступны 512-байтовые слоты a через g, а последние слоты h зарезервированы для метаданных (свободные слоты, следующие/предыдущие страницы в одном классе и т.д.), Обратите внимание, что чем больше размер класса, тем больше теряется в последнем слоте, поэтому более крупные распределения используют другую схему.

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

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


Что это означает для потребления памяти?

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

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

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

Конечно, игнорируя случай программы, которая делает выделение 1 М 1 байт, освобождает большинство из них таким образом, чтобы все страницы использовались, а затем делали то же самое с 2 байтами, 3 байтами и т.д.... но это похоже на патологический случай.

1 Да, я лежу сквозь зубы. Вам также необходимо учитывать затраты на внутренние структуры распределителя и тот факт, что он может кэшировать несколько неиспользуемых страниц ОС для подготовки к будущим распределениям,... тем не менее, это достаточно для объяснения эффекта фрагментации.


Итак, проблема фрагментации?

Ну, это все еще может быть. Адресное пространство все еще может быть фрагментировано, хотя и на уровне детализации страниц ОС.

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

И возникает проблема: эта иллюзия смежной памяти требует нахождения смежной области адресного пространства, которая подвержена фрагментации.

Эта фрагментация не отображается в небольших запросах, но для запросов по размеру страницы они могут быть проблемой. В наши дни с 64-битными указателями это на практике гораздо меньше (даже при использовании только 47 бит для пользователя), однако в 32-битных программах он имеет более высокую вероятность: например, это чрезвычайно сложно mmap создать 2GB файл в 32-битовом адресном пространстве, потому что он сразу занимает половину его... при условии, что его исключение не предотвратит (в этом случае запрос будет терпеть неудачу).


Потерялся ли бой?

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

Если ваша программа имеет поведение распределения, которое типичный распределитель плохо обрабатывает, вы можете:

  • взять под контроль конкретные распределения, которые вызывают проблемы (используя sbrk/mmap самостоятельно для их обработки)
  • или просто переписать специально настроенный распределитель

Через 10 лет я лично никогда не нуждался, и только писал распределители для развлечения в свободное время; но это возможно.

Ответ 2

Подводя итог Маттье, подробное объяснение -

Rust и C и С++ при использовании стандартного управления памятью приводят к фрагментированной памяти. Они не дефрагментируют.

Но в подавляющем большинстве случаев использования в реальном мире фрагментация настолько минимальна, что это не проблема.

Если это проблема, вы можете запустить собственный распределитель.