Динамическое распределение памяти является фундаментальной частью большинства компьютерных систем с 1960 года.1
Каждый использует динамическое распределение памяти. Если вы когда-либо называли malloc или new, то вы использовали динамическое распределение памяти. Большинство программистов склонны рассматривать кучу как& #8220;магический мешок& #8221;: Мы просим его о памяти, и он волшебным образом создает что-то для нас. Иногда мы сталкиваемся с проблемами, потому что куча не волшебная.
Куча ограничена. Даже в больших системах (т.е. не встроенных) с огромным количеством виртуальной памяти есть предел. Все знают о физическом пределе, но есть более тонкий, «виртуальный» предел, при котором ваша программа (или вся система) замедляется из-за использования виртуальной памяти. Этот виртуальный предел намного ближе к вашей программе, чем физический, особенно если вы работаете на многозадачной системе. Поэтому при работе на большой системе считаетсянеплохозаставить вашу программу использовать как можно меньше ресурсов и выпустить их как можно скорее. При использовании встроенной системы программисты обычно не теряют память.
Груда сложная. Он должен удовлетворять любой тип запроса памяти для любого размера и делать это быстро. Общие подходы к управлению памятью связаны с разделением памяти на части и упорядочением их по размеру в виде дерева или структуры списка. Добавьте другие факторы, такие как местность и оценка жизни, и кучи быстро становятся очень сложными. На самом деле, это настолько сложно, что не существует известногоидеальногоответа на вопрос о том, как сделать динамическое распределение памяти. Диаграммы ниже иллюстрируют, как работают наиболее распространенные менеджеры памяти: для каждого фрагмента памяти он использует часть этой памяти для поддержания своей внутренней структуры дерева или списка. Даже если какой-то фрагмент выводится в программу, менеджер памяти долженсохранитьнекоторую информацию в ней — обычно только ее размер. Затем, когда блок свободен, менеджер памяти может легко определить, насколько он велик.
Из-за усложнения динамического распределения памяти, он часто неэффективен с точки зрения времени и / или пространства. Большинство алгоритмов распределения памяти хранят некоторую форму информации с каждым блоком памяти, размером блока или некоторой реляционной информацией, такой как его положение во внутреннем дереве или структуре списка. Для такихполей заголовкаобычно используется одно машинное слово в блоке, который используется программой. Очевидным недостатком является то, что небольшие объекты динамически распределяются. Например, если инты были динамически распределены, то алгоритм автоматически зарезервирует пространство для полей заголовка, и мы в конечном итоге потеряем 50% памяти. Конечно, это наихудший сценарий. Однако более современные программы используют небольшие объекты на куче, что делает эту проблему все более очевидной. Уилсон и др. АЛ. утверждают, что средняя сумма накладных расходов на память составляет от десяти до двадцати процентов2. Накладные расходы на память будут расти, поскольку все больше программ используют более мелкие объекты. Именно накладные расходы на память приближают программы к виртуальному пределу.
В более крупных системах накладные расходы на память не так велики, как проблема (по сравнению с количеством времени, которое потребуется для работы вокруг нее), и поэтому часто игнорируются. Тем не менее, существуют ситуации, когда многие распределения и/или распределения меньших объектов происходят в рамках алгоритма с критичным временем, и в этих ситуациях система распределения памяти часто слишком медленна.
Простое отдельное хранилище решает обе эти проблемы. Почти все накладные расходы на память устраняются, и все выделения могут происходить в небольшом количестве (амортизированного) постоянного времени. Однако это делается с потерей общности; простое сегрегированное хранилище может выделять только куски памяти одного размера.
Простое сегрегированное хранилище является основной идеей библиотеки Boost Pool. Simple Segregated Storage — самый простой и, вероятно, самый быстрый алгоритм распределения памяти. Он начинается с разделения блока памяти на куски фиксированного размера. То, откуда происходит блок, не важно до момента реализации. Бассейн — это объект, который использует простое сегрегированное хранилище таким образом. Для иллюстрации:
Каждый из кусков в любом блоке всегда одинакового размера. Это фундаментальное ограничение простого сегрегированного хранения: вы не можете запрашивать куски разных размеров. Например, вы не можете попросить пул целых чисел для персонажа или пул символов для целого числа (при условии, что персонажи и целые числа имеют разные размеры).
Простая сегрегация Хранение работает путем чередования свободного списка в неиспользованных кусках. Например:
Перемещая свободный список в куски, каждое простое сегрегированное хранилище имеет только накладные расходы на один указатель (указатель на первый элемент в списке). Он не имеет накладных расходов на память для кусков, которые используются в процессе.
Простое сегрегированное хранение также очень быстрое. В простейшем случае выделение памяти — это просто удаление первой части из свободного списка, операция O(1). В случае, когда свободный список пуст, другой блок может быть приобретен и разделен, что приведет к амортизированному времени O(1). Распределение памяти может быть таким же простым, как добавление этого фрагмента в переднюю часть свободного списка, операция O(1). Однако для более сложного использования Simple Segregated Storage может потребоваться отсортированный бесплатный список, который делает распределение сделок O(N).
Простая сегрегация Хранение обеспечивает более быстрое выполнение и меньшие накладные расходы на память, чем у распределителя, поставляемого системой, но с потерей общности. Хорошее место для использования Бассейна находится в ситуациях, когда на кучу могут быть выделены многие (не смежные) мелкие объекты, или если распределение и распределение объектов одного размера происходит неоднократно.
Пересмотрите разделпонятий, если вы еще не знакомы с ним. Помните, что блок - это смежный раздел памяти, который разделен или разделен на куски фиксированного размера. Эти куски - это то, что выделяется и распределяется пользователем.
Каждый пул имеет один бесплатный список, который может распространяться на несколько блоков памяти. Таким образом, Бассейн также имеет связанный список выделенных блоков памяти. Каждый блок памяти по умолчанию выделяется с помощью<new[]>, и все блоки памяти освобождаются от разрушения. Именно использование<new[]>позволяет нам гарантировать выравнивание.
[5.3.3/2] (Выражения::Унитарные выражения::Sizeof)... При применении к массиву результатом является общее количество байтов в массиве. Это означает, что размер массива n элементов в n раз больше размера элемента.
Поэтому массивы не могут содержать набивку, хотя элементы внутри массивов могут содержать набивку.
[3.7.3.1/2] (Основные понятия: Продолжительность хранения:: Динамическая продолжительность хранения:: Функции распределения)"... Возвращаемый указатель должен быть соответствующим образом выровнен таким образом, чтобы его можно было преобразовать в указатель любого полного типа объекта и затем использовать для доступа к объекту или массиву в выделенном хранилище..."
[5.3.4/10] (Выражения::Единые выражения::Новые)"... Для массивов char и unsigned char разница между результатом нового выражения и адресом, возвращаемым функцией распределения, должна быть интегральной кратной наиболее строгому требованию (3.9) выравнивания любого типа объекта, размер которого не превышает размера создаваемого массива. [Примечание: Поскольку предполагается, что функции распределения возвращают указатели в хранилище, которое надлежащим образом выровнено для объектов любого типа, это ограничение на накладные расходы на распределение массивов допускает общую идиому распределения массивов символов, в которые впоследствии будут помещены объекты других типов».
Это следует из предикатов 1 и 2 и следующей цитаты:
[3.9/9] (Основные понятия::Типы)«Тип объекта — это (возможно, cv-квалифицированный) тип, который не является типом функции, не является эталонным типом и не является пустотным типом».
(В частности, типы массивов являются типами объектов.)
Нет цитат из Стандарта, чтобы непосредственно поддержать этот аргумент, но он соответствует общей концепции значения «выравнивания».
Следует отметить, что условия для<p+i>, которые должны быть четко определены, изложены в [5.7/5]. Мы не цитируем это здесь, но только заметим, что это четко определено, если<p>и<p+i>оба указывают на один и тот же массив.
Это следует естественно, поскольку блок памяти представляет собой массив Элементов, и для каждого n,<sizeof(Element)%sizeof(Tn)==0;>таким образом, граница каждого элемента в массиве Элементов также является границей каждого элемента в каждом массиве Tn.
Вышеприведенное доказательство охватывает требования к выравниванию для вырезания кусков из блока. В реализации используются фактические размеры объектов:
Запрашиваемый размер объекта<requested_size>; это размер кусков, запрашиваемых пользователем
<void*>(указатель на пустоту); это потому, что мы переплетаем наш свободный список через куски.
<size_type>Это потому, что мы сохраняем размер следующего блока в каждом блоке памяти.
Каждый блок также содержит указатель на следующий блок; но он хранится как указатель на пустоту и отбрасывается при необходимости, чтобы упростить требования к выравниванию для трех типов выше.
Таким образом,<alloc_size>определяется как самый большой из вышеперечисленных размеров, округленный до множества из всех трех размеров. Это гарантирует выравнивание при условии, что все выравнивания являются полномочиями двух: что-то, что кажется верным на всех известных платформах.
Каждый блок памяти состоит из трех основных разделов. Первый раздел - это часть, из которой вырезаны куски, и содержит переплетенный бесплатный список. Второй раздел — указатель на следующий блок, а третий — размер следующего блока.
Каждая из этих секций может содержать прокладку по мере необходимости, чтобы гарантировать выравнивание для каждой из следующих секций. Размер первой секции<number_of_chunks*lcm(requested_size,sizeof(void*),sizeof(size_type));>, размер второй секции<lcm(sizeof(void*),sizeof(size_type);>и размер третьей секции<sizeof(size_type)>.
Вот пример блока памяти, где<requested_size==sizeof(void*)==sizeof(size_type)==4>:
Чтобы показать визуальный пример возможной прокладки, вот пример блока памяти, где<requested_size==8andsizeof(void*)==sizeof(size_type)==4>
Вышеприведенная теорема гарантирует все требования к выравниванию для распределения кусков, а также детали реализации, такие как переплетенный бесплатный список. Тем не менее, он делает это, добавляя набивку, когда это необходимо; поэтому мы должны относиться к выделению смежных кусков по-другому.
Используя массив аргументов, аналогичных приведенным выше, мы можем перевести любой запрос на непрерывную память для<n>объектов<requested_size>в запрос на m смежных кусков.<m>— это просто<ceil(n*requested_size/alloc_size)>, где<alloc_size>— фактический размер кусков.
Для иллюстрации:
Вот пример блока памяти, где<requested_size==1>и<sizeof(void*)==sizeof(size_type)==4>:
Затем, когда пользователь распределяет смежную память, мы можем снова разделить ее на куски.
Отметим, что реализация, предусмотренная для выделения смежных кусков, использует линейный вместо квадратичного алгоритм. Это означает, что он не может найти смежные свободные куски, если свободный список не упорядочен. Таким образом, рекомендуется всегда использовать упорядоченный бесплатный список при работе с смежным распределением кусков. (В приведенном выше примере, если бы Chunk 1 указал на Chunk 3 указал на Chunk 2 указал на Chunk 4, вместо того, чтобы быть в порядке, алгоритм непрерывного распределения не смог бы найти какой-либо из смежных кусков).
<simple_segregated_storage.hpp>предоставляет класс шаблонов simple_segregated_storage, который контролирует доступ к свободному списку фрагментов памяти.
Отметим, что это очень простой класс, с непроверенными предпосылками практически на все его функции. Он предназначен для того, чтобы быть самым быстрым и самым маленьким из возможных быстрораспределителей памяти, например, что-то, что можно использовать во встроенных системах. Этот класс делегирует много сложных предварительных условий пользователю (особенно проблемы выравнивания). Для более общего использования см. другиеИнтерфейсы пула.
Объект типа<simple_segregated_storage<SizeType>>пуст, если его свободный список пуст. Если он не пустой, то заказывается, если заказывается его бесплатный список. Свободный список упорядочен, если повторные вызовы<malloc()>приведут к постоянно увеличивающейся последовательности значений, как определено<std::less<void*>>. Функция-член сохраняет порядок, если свободный список сохраняет свою ориентацию порядка (то есть упорядоченный свободный список все еще упорядочен после вызова функции-члена).
Table 1. Symbol Table
символ
значение
Магазин
simple_segregated_storage
t
стоимость магазина типа
u
значение типа const Store
блок, кусок, конец
значения типа void *
Раздел_sz, sz, n
значения типа Store::size_type
Table 2. Template Parameters
Параметр
по умолчанию
Требования
Тип размера
std::size_t
Неподписанный интегральный тип
Table 3. Typedefs
символ
Тип
size_type
Тип размера
Table 4. Constructors, Destructors, and State
выражение
Тип возврата
Пост-состояние
Заметки
Магазин()
Не используется
пустой
Строительство нового магазина
(&t)->~Store()
Не используется
Разрушает магазин
U.empty()
bool
Returns true if u is empty. Order-preserving.
Table 5. Segregation
выражение
Тип возврата
Предварительное состояние
Пост-состояние
Семантическая эквивалентность
Заметки
Магазин::сегрегация (блок, sz, раздел_sz, конец)
void
partition_sz >= sizeof(void *) partition_sz = sizeof(void *) * i, для некоторого целого числа i sz >= partition_sz block правильно выровнен для массива объектов размера partition_sz block правильно выровнен для массива пустоты *
Переносит свободный список через блок памяти, указанный блоком размера sz байтов, разделяя его на как можно больше кусков размером с partition_sz. Последний кусок устанавливается, чтобы указать на конец, и указатель на первый член возвращается (это всегда равно блоку). Этот переплетенный бесплатный список заказывается. O(sz).
Магазин::segregate(блок, sz, partition_sz)
void
Same as above
Магазин::segregate(блок, sz, partition_sz, 0)
t.add_block(block, sz, partition_sz)
пустота
Same as above
!t.empty()
Разделяет блок памяти, указанный блоком размера sz байтов, на фрагменты размером с partition_sz и добавляет этот бесплатный список к своему собственному. Если t было пусто до этого звонка, то оно заказывается после этого звонка. O(sz).
t.add_ordered_block(block, sz, partition_sz)
пустота
Same as above
!t.empty()
Разделяет блок памяти, указанный блоком размера sz байтов, на фрагменты размером с partition_sz и объединяет этот свободный список в свой собственный. Сохранение порядка. O(sz).
Table 6. Allocation and Deallocation
выражение
Тип возврата
Предварительное состояние
Пост-состояние
Семантическая эквивалентность
Заметки
t.malloc
void
!t.empty()
Берет первый доступный кусок из бесплатного списка и возвращает его. Сохранение порядка. О(1).
t.free
пустота
chunk was previously returned from a call to t.malloc()
!t.empty()
Места в свободном списке. Обратите внимание, что кусочек не может быть 0. O(1).
t.ordered_free(chunk)
пустота
Same as above
!t.empty()
Места в свободном списке. Обратите внимание, что кусочек может не быть 0. Сохранение порядка. O(N) относительно размера бесплатного списка.
t.malloc_n(n, partition_sz)
void
Попытки найти прилежащую последовательность n кусков размером с z. Если они найдены, удалите их все из бесплатного списка и верните указатель на первый. Если не найдено, возвращается 0. Настоятельно рекомендуется (но не обязательно) упорядочить свободный список, так как этот алгоритм не сможет найти непрерывную последовательность, если она не является смежным в свободном списке. Сохранение порядка. O(N) относительно размера бесплатного списка.
t.free_n(chunk, n, partition_sz)
пустота
chunk was previously returned from a call to t.malloc_n(n, partition_sz)
!t.empty()
t.add_block(chunk, n * partition_sz, partition_sz)
Предполагает, что кусочек на самом деле относится к блоку кусков, охватывающих байты n * partition_sz; разделяет и добавляет в этот блок. Обратите внимание, что кусочек может не быть 0. O(n).
t.ordered_free_n(chunk, n, partition_sz)
пустота
Как и выше
Как и выше
t.add_ordered_block(chunk, n * partition_sz, partition_sz)
То же, что и выше, за исключением того, что он сливается в свободном списке. Сохранение порядка. O(N + n) где N - размер свободного списка.
Объекты пула должны запрашивать блоки памяти из системы, которые пул затем разделяет на куски, чтобы выделить пользователю. Указывая параметр шаблона UserAllocator для различных интерфейсов Pool, пользователи могут контролировать распределение блоков системной памяти.
В следующей таблицеUserAllocatorявляется типом User Allocator,блокявляется значением типа char *, аnявляется значением типа. UserAllocator::size_type
Table 7. UserAllocator Requirements
выражение
Результат
Описание
UserAllocator::size_type
An unsigned integral type that can represent the size of the
largest object to be allocated.
UserAllocator::difference_type
Подписанный интегральный тип, который может представлять разницу любых двух указателей.
UserAllocator::malloc(n)
char *
Попытки выделить из системы n байт. Возвращает 0, если вне памяти.
UserAllocator::free(block)
пустота
блок должен быть ранее возвращен с вызова в UserAllocator::malloc.
Статья Pool in More Depth раздела Boost.Pool Introduction and Overview может быть полезна для разработчиков на c++ и boost.
Материалы статей собраны из открытых источников, владелец сайта не претендует на авторство. Там где авторство установить не удалось, материал подаётся без имени автора. В случае если Вы считаете, что Ваши права нарушены, пожалуйста, свяжитесь с владельцем сайта.