предложения по улучшению реализации алгоритма распределителя

У меня есть приложение Visual Studio 2008 C++, в котором я использую настраиваемый распределитель для стандартных контейнеров, так что их память поступает из файла с отображением памяти, а не из кучи. Этот распределитель используется для 4 различных вариантов использования:

  1. 104-байтовая структура фиксированного размера std::vector< SomeType, MyAllocator< SomeType > > foo;
  2. 200-байтовая структура фиксированного размера
  3. 304-байтовая структура фиксированного размера
  4. n-байтовые строки std::basic_string< char, std::char_traits< char >, MyAllocator< char > > strn;

Мне нужно иметь возможность выделить примерно 32 МБ для каждого из них.

Распределитель отслеживает использование памяти, используя std::map указателей на размер выделения. typedef std::map< void*, size_t > SuperBlock; Каждый SuperBlock представляет собой 4 МБ памяти.

Их std::vector< SuperBlock > на случай, если одному SuperBlock недостаточно места.

Алгоритм, используемый для распределителя, выглядит следующим образом:

  1. Для каждого суперблока: есть ли место в конце суперблока? поместите выделение туда. (быстрый)
  2. Если нет, найдите в каждом суперблоке пустое пространство достаточного размера и поместите выделение туда. (медленный)
  3. Еще ничего? выделить еще один суперблок и поместить выделение в начало нового суперблока.

К сожалению, через некоторое время шаг 2 может стать ОЧЕНЬ медленным. Когда создаются копии объектов и уничтожаются временные переменные, я получаю большую фрагментацию. Это вызывает много глубоких поисков в структуре памяти. Проблема фрагментации, так как у меня ограниченный объем памяти для работы (см. примечание ниже).

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

Примечание. Для тех, кому нужна причина: я использую этот алгоритм в Windows Mobile, где ограничение слота процесса для кучи составляет 32 МБ. Так что обычный std::allocator не подойдет. Мне нужно разместить выделения в большой области памяти объемом 1 ГБ, чтобы было достаточно места, и это то, что это делает.


person PaulH    schedule 17.05.2011    source источник


Ответы (6)


Для объектов фиксированного размера вы можете создать распределитель фиксированного размера. По сути, вы выделяете блоки, разбиваете их на подблоки соответствующего размера и создаете связанный список с результатом. Выделение из такого блока — O(1), если есть доступная память (просто удалите первый элемент из списка и верните на него указатель), как и освобождение (добавьте блок в список свободных). Во время выделения, если список пуст, захватите новый суперблок, раздел и добавьте все блоки в список.

Для списка переменного размера вы можете упростить его до блока фиксированного размера, выделив только блоки известных размеров: 32 байта, 64 байта, 128 байтов, 512 байтов. Вам нужно будет проанализировать использование памяти, чтобы придумать разные сегменты, чтобы не тратить слишком много памяти. Для больших объектов вы можете вернуться к шаблону динамического выделения размера, который будет медленным, но, надеюсь, количество больших объектов ограничено.

person David Rodríguez - dribeas    schedule 17.05.2011
comment
Хорошая идея сочетать фиксированный размер и переменный размер. Я только что реализовал это, и это действительно очень быстро. Спасибо. - person PaulH; 17.05.2011
comment
Вы должны прочитать ответ Матье М., он гораздо более полный и довольно хороший и решает большое количество проблем, с которыми вы столкнетесь, если развернете свои собственные распределители. - person David Rodríguez - dribeas; 18.05.2011

Можете ли вы иметь отдельный пул выделения памяти для каждого типа фиксированного размера, который вы выделяете? Таким образом не будет никакой фрагментации, потому что выделенные объекты всегда будут выравниваться по n-байтовым границам. Это, конечно, не помогает для строк переменной длины.

В Modern C++ Александреску есть пример выделения небольших объектов. дизайн, который иллюстрирует этот принцип и может дать вам некоторые идеи.

person Tim Martin    schedule 17.05.2011
comment
Это хороший способ ускорить большую часть распределений. Спасибо за идею. +1 - person PaulH; 17.05.2011

Основываясь на ответе Тима, я бы лично использовал что-то вроде BiBOP.

Основная идея проста: используйте пулы фиксированного размера.

К этому есть некоторые уточнения.

Во-первых, размер пулов обычно фиксирован. Это зависит от вашей процедуры распределения, обычно, если вы знаете ОС, с которой вы работаете, на карте как минимум 4 КБ одновременно, когда вы используете malloc, тогда вы используете это значение. Для файла с отображением памяти вы можете увеличить это значение.

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

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

Во-вторых, как обращаться с пулами? Использование связанных списков.

Страницы обычно не типизированы (сами по себе), поэтому у вас есть свободный список страниц, в котором можно подготовить новые страницы и поместить «переработанные» страницы.

Для каждой категории размера у вас есть список «занятых» страниц, в которых выделена память. Для каждой страницы, которую вы храните:

  • размер выделения (для этой страницы)
  • количество выделенных объектов (для проверки на пустоту)
  • указатель на первую свободную ячейку
  • указатель на предыдущую и следующую страницы (может указывать на «голову» списка)

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

Список «занятых» страниц заданного размера управляется просто:

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

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

Для многопоточных / многопроцессорных приложений вам необходимо добавить синхронизацию (обычно мьютекс на страницу), если вы можете получить вдохновение от tcmalloc Google (попробуйте найти другую страницу вместо блокировки, используйте локальный кеш потока чтобы вспомнить, какую страницу вы использовали в последний раз).

Сказав это, вы пробовали Boost.Interprocess ? Он предоставляет распределители.

person Matthieu M.    schedule 17.05.2011

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

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

person Mark B    schedule 17.05.2011

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

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

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

person supercat    schedule 17.05.2011
comment
Это выглядит очень интересно. Если мне нужна дополнительная скорость для выделения больших блоков переменного размера, я, вероятно, попробую это. +1 - person PaulH; 17.05.2011
comment
@PaulH: я не знаю, каковы ваши потребности в отношении времени наихудшего случая по сравнению со средним временем, но вы можете поиграть с размерами своих суперблоков. Кроме того, если у вас есть некоторые априорные знания о том, что некоторые распределения будут недолговечными, а другие — более продолжительными, было бы полезно поместить объекты с аналогичным ожидаемым временем жизни в один и тот же суперблок. В идеальном случае некоторые суперблоки будут часто заполняться, но к тому времени, когда они заполнятся, большая часть содержимого в них будет удалена, и придется копировать очень мало материала. - person supercat; 18.05.2011

Я согласен с Тимом — используйте пулы памяти, чтобы избежать фрагментации.

Тем не менее, вы можете избежать некоторых проблем, сохраняя указатели, а не объекты в ваших векторах, возможно, ptr_vector?

person Douglas Leeder    schedule 17.05.2011
comment
Вау! Переключение на ptr_vector для тех типов, которые могли его использовать, имело огромное значение. Спасибо! - person PaulH; 18.05.2011