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

#c #pointers #memory-management #invalidation #memory-pool

#c #указатели #управление памятью #недействительность #пул памяти

Вопрос:

Термины «пул» и «буфер» могут использоваться здесь взаимозаменяемо.
Предположим, у меня есть пул, который я хочу выделить в начале программы, чтобы не вызывать new постоянно.
Я не хочу искусственно ограничивать себя в размере пула, но если я перераспределю пул большего размера, все указатели на старый будут признаны недействительными, что, конечно, не очень круто.


Одним из способов, о котором я подумал, была «подкачка», иначе

 const int NUM_PAGES = 5;
char* pool[NUM_PAGES];
  

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


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


Какие еще возможные способы достичь того, чего я хочу, существуют? Какие (отрицательные) преимущества я упустил в приведенных выше примерах реализаций?

Ответ №1:

Вы говорите о чем-то, что напоминает мне о std::deque . Я не совсем уверен, можете ли вы использовать std::deque как есть, или вам просто нужно использовать его базовый дизайн для реализации какого-то распределителя.

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

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

2. В итоге я использовал deque. Не знал о специальных свойствах увеличения deque, спасибо!

Ответ №2:

Продолжая вашу концепцию выгружаемого «пула», что, если бы вы сохранили страницы в виде связанного списка??

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

Я использую в основном ту же идею для объединенных распределителей, но также с «свободным списком» недавно освобожденных элементов…

РЕДАКТИРОВАТЬ: Согласно вашему комментарию, если вы хотите также использовать освобожденные данные, вы также можете сохранить свободный список, возможно, также в виде связанного списка. Итак, когда вы освобождаете данные, вы помещаете указатель и маркер размера в список свободных. Когда вы выделяете данные из пула, вы сначала проверяете, можно ли использовать какие-либо элементы из списка свободных, если вы не выделяете их из пула.

Стандартные модули управления памятью часто уже делают что-то подобное, поэтому этот подход не всегда будет лучше. В частности, я обычно использую этот тип пользовательского распределителя только тогда, когда выделенные элементы имеют одинаковый размер (так что обход свободного списка равен O (1)!). Пользовательский распределитель для std::list был бы одним из примеров.

Надеюсь, это поможет.

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

1. Другой (теперь удаленный) ответ также навел меня на эту идею. Как упоминалось в моем вопросе об общей идее «выгрузки», требуется больше усилий для организации, например, если указатель на «старой» странице освобождается — как бы вы организовали эффективное повторное использование этого пространства?

2. 1 за то, что этот подход не всегда будет лучше , вы добавляете сложности, и, возможно, оно того не стоит: измерьте, прежде чем добавлять сложность!. Как насчет хранения фактических указателей страниц в vector (или deque ? Я не совсем вижу необходимости в списке (обычно его сложнее поддерживать), и перераспределение указателей на страницы на самом деле не должно быть проблемой, поскольку указатели, которые есть в пользовательском коде, относятся к содержимому страниц, а не к указателям.

Ответ №3:

Рассмотрите возможность использования пулов повышения

Ответ №4:

Один вопрос, конечно, заключается в том, зачем себя так беспокоить?

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

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

Мой личный интерес склоняется к стратегии BiBOP (Большой пакет страниц) для борьбы с фрагментацией. Идея в том, чтобы иметь выделенный пул для каждого размера выделения, и, таким образом, простого свободного списка (чередующегося с выделениями) достаточно (слияние не требуется). Обычно это делается до тех пор, пока запрашиваемый размер меньше размера страницы (я видел, что используется 4 КБ), а все, что больше, выделяется само по себе (на нескольких страницах). Удаленные страницы перерабатываются.

Меня больше всего интересует то, что с помощью BiBOP легко поддерживать адрес дерева интервалов-диапазон -> заголовок страницы, таким образом определяя полный размер объекта по адресу (возможно) внутреннего элемента (например, атрибута), который полезен для сборки мусора (ссылка на следующий шаг).

Для многопоточного распределения tcmalloc и jemalloc используйте два разных подхода:

  • jemalloc использовать пул потоков: хорошо с фиксированным количеством потоков / пулов потоков, но делает процесс создания потока более дорогостоящим
  • tcmalloc используйте глобальный многопоточный пул с технологией без блокировок и попытайтесь сбалансировать нагрузку потоков в доступных пулах, чтобы ограничить конкуренцию, заставляя поток искать новый пул, если тот, который он использует, «заблокирован» (вместо ожидания), и заставляя каждый поток кэшировать последний использованный пул в локальной переменной потока. Поэтому потоки являются легковесными, но могут возникнуть некоторые разногласия, если количество пулов слишком мало по сравнению с количеством активных потоков.

Ответ №5:

Несколько мыслей:

  • Когда у вас есть std::vector<T*> , добавление элементов и запуск изменения размера делает недействительными ссылки / указатели / итераторы в этот контейнер, но не ссылки / указатели, которые напрямую обращаются к указанным объектам. Итак, уровень косвенности может решить вашу проблему, в зависимости от того, что вы действительно пытаетесь сделать с этими ссылками / указателями / итераторами.

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

  • Другие контейнеры — std::map<> и std::list<> — не перемещают свои существующие элементы по мере добавления новых, поэтому итераторы / указатели / ссылки остаются действительными.

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

1. такие системы : Linux, например.

2. @Matthieu: Linux определенно удовлетворяет аспекту виртуальной памяти; 32-разрядная сборка, в которой вы хотите иметь дело с гигабайтами данных, может не соответствовать критериям «большого адресного пространства»… :-/