Управление памятью вручную
Берём выделение памяти под контроль: арены, пулы, placement new и выравнивание
new/delete и malloc/free - это аллокаторы общего назначения: они обязаны обслуживать любой размер, любое время жизни и любой поток. За эту универсальность приходится платить временем, кэшем и фрагментацией. Пользовательский аллокатор сознательно отказывается от универсальности - вы знаете шаблон выделения памяти в своей программе, поэтому можете возвращать память быстрее и плотнее, чем это когда-либо сделает аллокатор общего назначения. Эта тема о том, как отделить выделение памяти от конструирования и взять под контроль и то, и другое.
- объяснить, почему универсальная куча медленна и фрагментирует память,
- использовать
placement new, чтобы сконструировать объект в уже принадлежащей вам памяти, и разрушить его вручную, - построить арену (bump-аллокатор) и пул фиксированного размера и понимать, когда уместен каждый,
- рассуждать о выравнивании с помощью
alignof,alignasиstd::align, - подключить собственный ресурс памяти к STL через C++17
<memory_resource>.
1. Чем плоха универсальная куча
Каждый вызов ::operator new или malloc может делать неожиданно много работы: искать блок в списке свободных участков или в корзинах классов размеров, разбивать или сливать блоки, обновлять служебные заголовки и - при конкуренции - брать блокировку. Возвращаемый блок несёт скрытые накладные расходы на каждое выделение (слово размера/заголовка, округление до класса размера) и оказывается там, где аллокатор нашёл место, а не рядом с соседями. Для продвинутого кода важны три цены:
- Задержка: одно выделение может стоить сотни наносекунд; в горячем цикле это доминирует.
- Фрагментация: после множества разноразмерных выделений и освобождений свободная память дробится на слишком мелкие для переиспользования кусочки. Внешняя фрагментация тратит место между блоками; внутренняя - на дополнении внутри округлённого блока.
- Кэш-локальность: объекты, используемые вместе, но выделенные далеко друг от друга, вызывают промахи кэша. Пользовательский аллокатор может уложить их подряд.
Решение - не "написать более быстрый malloc". Оно в том, чтобы использовать шаблон, о котором аллокатор общего назначения не может знать: "все эти объекты умирают одновременно" (арена) или "каждый объект ровно одного размера" (пул). Когда шаблон выполняется, аллокатор сводится к сдвигу указателя или снятию элемента со списка - несколько инструкций, без поиска, без блокировки.