Что позволяет std::pmr::polymorphic_allocator выбирать стратегию выделения памяти во время выполнения без изменения типа контейнера?
std::pmr::polymorphic_allocator хранит указатель на объект std::pmr::memory_resource и делегирует ему операции выделения и освобождения памяти. Поэтому один и тот же тип контейнера, например std::pmr::vector<int>, может использовать разные стратегии: обычную кучу, монотонный буфер, пул объектов или пользовательский ресурс.
Тип контейнера при этом не кодирует конкретный аллокатор. Стратегия выбирается через объект memory_resource, переданный контейнеру во время выполнения.
До C++17 аллокаторы контейнеров уже существовали, но их использование часто усложняло типы и взаимодействие между контейнерами. Конкретный тип аллокатора обычно становился частью типа контейнера, что затрудняло передачу контейнеров между компонентами с разными стратегиями управления памятью.
Полиморфные аллокаторы и memory resource из C++17 отделили интерфейс контейнера от конкретной реализации выделения памяти. Это особенно полезно для серверов, встроенных систем и приложений, где важны предсказуемые задержки, локальность данных или пакетное освобождение памяти.
Предположим, компонент строит временный граф объектов на время обработки одного запроса. Использование обычного std::vector приводит к множеству индивидуальных выделений в общей куче, а освобождение объектов выполняется постепенно и может создавать дополнительную нагрузку на аллокатор.
Если заменить контейнер на std::pmr::vector, можно направить его выделения в заранее подготовленный ресурс. Неверный выбор ресурса, однако, опасен: монотонный ресурс не освобождает отдельные блоки при уничтожении элементов, а ресурс должен жить дольше всех контейнеров, которые к нему обращаются.
std::pmr::polymorphic_allocator<T> содержит ссылку или указатель на std::pmr::memory_resource. При запросе памяти аллокатор вызывает виртуальные операции ресурса: allocate, deallocate и, при необходимости, is_equal для сравнения ресурсов.
Контейнер std::pmr::vector<T> является алиасом для std::vector<T, std::pmr::polymorphic_allocator<T>>. Его тип не меняется при замене ресурса, но фактическое поведение выделения памяти меняется.
В примере контейнер сначала использует предоставленный буфер, а затем при нехватке места обращается к ресурсу верхнего уровня. monotonic_buffer_resource освобождает память не по отдельным операциям deallocate, а целиком при уничтожении самого ресурса или явном вызове release.
Это даёт быстрые массовые аллокации и освобождения, но не подходит для долгоживущих контейнеров с частым удалением элементов: освобождённые отдельные блоки не возвращаются для повторного использования внутри ресурса. Для такой задачи лучше рассмотреть unsynchronized_pool_resource или synchronized_pool_resource.
Главное ограничение — время жизни. Ресурс должен существовать дольше каждого контейнера и объекта, использующего его аллокатор. Кроме того, перенос контейнера между потоками безопасен только при соблюдении правил безопасности конкретного ресурса: unsynchronized_pool_resource не предназначен для одновременного доступа из нескольких потоков.
Полиморфный вызов виртуальных функций и косвенное обращение к ресурсу имеют небольшую стоимость. Поэтому PMR не следует применять автоматически: для простого контейнера с обычным временем жизни стандартный std::vector часто проще и эффективнее.
Сервис обрабатывает запрос, создавая тысячи небольших временных объектов. После завершения обработки все они должны быть освобождены одновременно.
Вариант с обычными контейнерами использует глобальную кучу. Его плюсы — простота и привычное поведение; минусы — множество аллокаций и отсутствие пакетного освобождения.
Вариант с отдельным std::pmr::monotonic_buffer_resource на запрос позволяет сначала использовать локальный буфер, а затем освободить все блоки одним действием. Минус — память нельзя эффективно вернуть по одному удалённому объекту, поэтому ресурс нельзя бездумно делать долгоживущим.
Практичное решение — создать ресурс на время обработки запроса, передать его всем std::pmr-контейнерам этого запроса и уничтожить после завершения обработки. Это уменьшает число обращений к общей куче и делает границу освобождения памяти явной.
Что произойдёт, если уничтожить memory_resource раньше контейнера?
Контейнер сохранит указатель на ресурс, но этот указатель станет висячим. При последующем выделении, освобождении памяти или уничтожении контейнера поведение будет неопределённым. Поэтому ресурс обычно размещают во внешней области, охватывающей все использующие его контейнеры.
Заменяет ли PMR типобезопасность обычного аллокатора динамическим приведением типов?
Нет. polymorphic_allocator<T> остаётся типизированным аллокатором для T. Полиморфным является объект memory_resource, через интерфейс которого выполняются операции с сырыми блоками памяти. Конструирование и уничтожение объектов по-прежнему выполняет контейнер с учётом типа элемента.
Почему нельзя считать monotonic_buffer_resource универсальной более быстрой заменой обычной куче?
Он эффективен, когда объекты живут примерно до конца одной логической фазы и освобождаются группой. Но при длительном времени жизни и частых удалениях память может накапливаться до вызова release или уничтожения ресурса. Для повторного использования небольших блоков лучше подходит пуловый ресурс, а для обычных непредсказуемых нагрузок — стандартный аллокатор.