Хочу поделиться своим опытом разработки крупных игровых проектов на C++, где производительность и стабильность — это не просто приятные бонусы, а абсолютно естественные требования к разработке. За годы работы над движками и играми я понял, что подход к управлению памятью очень сильно влияет на весь проект. В отличие от многих приложений - игры, особенно большие, часто работают часами без прерываний и должны поддерживать стабильный фреймрейт и отзывчивость. Когда проседание fps или фриз происходит на глазах у сотен тысяч игроков, вам уже никто не поможет — ущерб уже нанесен, а в steam полетели отзывы о кривизне рук разработчиков.
Недавно моя команда закончила работу над довольно старым и известным проектом, который портировали больше двух лет на плойку. Движок старый, большой и мощный, но работа с памятью была ориентирована на ПК времен конца 2000-х, и что меня поразило, так это насколько сильно большая часть кодовой базы зависела от динамической памяти во время выполнения. На ограниченном железе (далеко не у всех есть PS5 pro) и в условиях жёстких требований к сертификации на консолях такие решения быстро превращаются в проблему.
В разработке для консолей (про мобильные устройства я молчу, потому что игра не влезает по памяти даже в восемь гигов) с ограниченными ресурсами, архитектура с частыми аллокациями не просто неэффективна — она становится реальной угрозой для стабильности проекта. Каждое выделение памяти в куче влечёт за собой накладные расходы: это дополнительные !миллисекунды! (в целом на кадре) задержки, риск большой фрагментации памяти, и непредсказуемое поведение в долгой игровой сессии. После двух часов игры постоянные операции с кучей буквально «сжигают» половину бюджета кадра.

Про сотни тысяч игроков я не приукрасил, ежедневное число игроков проекта моей команды частенько переваливает за 500к в стиме.
Динамическая память — это проблема в разработке игр
В играх и игровых движках, особенно на консолях и мобильных устройствах, управление памятью должно быть максимально предсказуемым. Это значит:
никакой неожиданной задержки из-за фрагментации кучи;
никакого риска падения fps из-за нехватки памяти в разгаре боя или на важной сцене;
никакого постепенного ухудшения производительности во время игры;
никаких ошибок выделения памяти, которые могут сорвать катку или вызвать вылет у игроков.
В отличие от десктопных приложений, где пользователь может «перезапустить» программу и продолжить что-то делать - игра должна стабильно работать на ограниченном железе с фиксированным объёмом памяти. Если память заканчивается, а она физически заканчивается — игра крашится. В условиях консоли или мобильного устройства это не теоретическая угроза, а практическая реальность, которая напрямую влияет на опыт тысяч игроков.
Консоль |
Общий объем |
Доступно для игры |
Особенности архитектуры |
PlayStation 5 |
16 Gb GDDR6 |
12.5-13 Gb |
Единая архитектура |
Xbox Series X |
16 Gb GDDR6 |
~11.5 Gb |
10 Gb высокоскоростной |
Xbox Series S |
10 Gb GDDR6 |
~7 Gb |
8 Gb высокоскоростной |
Скрытые издержки динамической памяти
Мои замеры показывают, что время выделения памяти в куче деградирует при длительной (порядка трех часов) игре в 2–5 раз на xbox и 2-3 раза на playstation5, напрямую влияя на производительность игры. На мобильниках такие длинные сессии редкость, но там фрагментация со временем может «съедать» до 30% доступной памяти в длинных (более получаса) игровых сессиях, что для платформ с ограниченными ресурсами это означает не только падение FPS, но и фактические вылеты по ООМ.
Это конечно мелочь, но разные реализации malloc
добавляют 24–64 байт накладных расходов на каждое выделение для служебной информации. И если в игре, где за кадр происходят тысячи мелких аллокаций, я не ошибся - тысячи за кадр (например, при создании объектов или эффектов), этот оверхед сам по себе занимает какую-то часть памяти.
Аллокатор |
Приблизный объём служебных данных на одно выделение |
Комментарии |
---|---|---|
glibc malloc |
16-24 байт |
Хранит размер блока + флаги + указатели/связи в списках свободных блоков |
jemalloc |
“отдельно от блоков”, но накладные расходы всё равно есть — несколько байт за блок, и дополнительная структура |
накладные расходы зависят от размера выделения и класса. |
TCMalloc |
32+ байт / класс — зависит от „size-class“ + кеша потоков + страниц |
дополнительные расходы на данные о кешах потоков, управления “size-classes”, накладные расходы выше для мелких аллокаций. |
Windows/Xbox |
48+(debug) / 30+ байт (релиз) |
Зависит от версии OS, режима (отладочный / релизный) и архитектуры |
PlayStation |
минимально 12 байт (размер + урезанный указатель на следующий блок + флаги) |
Нет точной цифры, зависит от версии SDK. |
А насыпьте мне кода без кучи...
Я, как и многие мои знакомые из мира игростроя, считаю, что современные возможности языка стали слишком «тяжелые» или лишними для разработки игр, но весь это вкусный сахар, тем не менее позволяет писать физически меньше кода. Можно успешно использовать лямбды, RAII, статический полиморфизм и даже еще не полностью изученные возможности C++23 для создания игр, не отказываться от современных инструментов, а применять их разумно. И, конечно, надо понимать ограничения наших систем и использовать только те возможности языка, которые не нарушают требований к производительности и предсказуемости.
В какой-то момент команда пришла к пониманию, что нужны аналоги привычных STL-контейнеров, но с фиксированным размером. Например, gtl::vector<T, N>
у нас работает точно так же, как std::vector<T>
, но может содержать максимум N элементов. Это означает, что вся память для элементов выделяется в момент создания объекта, а не динамически при добавлении элементов. Но лень, старые привычки и реактивность мозга не дают возможности писать сразу без ошибок, а ведь такой подход сулит множество преимуществ для разработки. Во-первых, размер контейнера известен на этапе компиляции, что позволяет статически анализировать потребление памяти. Во-вторых, операции добавления и удаления элементов выполняются за предсказуемое время, поскольку не требуют обращений к системе управления памятью. Это важно для понимания куда уходит время на кадре, а еще можно рисовать красивые презентации начальству, как мы тут боремся за перф.
Стандартный std::function
в C++ использует динамическое выделение памяти для хранения больших объектов, что вообще неприемлемо для игр, когда каждый второй обработчик начинает использовать лямбду, обернутую в функтор. Есть несколько библиотек, которые решают эту проблему, например библитека FastDelegate (Don Clugston), написанная лет двацать назад (ссылка), но не утратившая своей актуальности или реализация функторов от ETL (ссылка)etl::function<Signature, StorageSize>
, где используется буфер для хранения функционального объекта, в самом функторе, и еще как минимум пара хороших библиотек на гитхабе. Это позволяет использовать все преимущества функционального программирования - лямбда-выражения, функторы, указатели на функции — без риска неконтролируемого выделения памяти. Т.е. мы сами определяем максимальный размер функционального объекта, и если он превышает заданный лимит, компилятор просто выдаст ошибку. Теперь частенько мой код выглядит вот так:
// <<<< std::vector<int>
gtl::vector<int, 64> _unit_options;
// <<<< std::function<void()>
gtl::function<void(), 32> _unit_death_cb;
Если программист пытается добавить элемент в уже заполненный контейнер или сохранить слишком большой функциональный объект, код просто не скомпилируется и это намного лучше, чем получить ошибки в рантайме. Такой подход позволяет выявить потенциальные проблемы еще до того, как программа попадет к игроку, а статический анализ кода становится более эффективным, поскольку компилятор может точно определить максимальное потребление памяти.
... и добавьте немного CRTP
В традиционном ООП на плюсах мы часто используем виртуальные функции для достижения полиморфизма. Когда у нас есть базовый класс с виртуальными методами и несколько наследников. Это обычный подход, мы перекладываем часть работы на компилятор, который создает специальную таблицу виртуальных функций (vtable). При вызове метода программа сначала обращается к этой таблице, чтобы определить, какую именно функцию нужно вызвать.
Этот механизм создает несколько проблем, и хоть они уже не столь критичны, как это было десять-пятнадцать лет назад, сами то проблемы никуда не ушли, просто процессоры стали быстрее. Во-первых, каждый вызов виртуальной функции требует дополнительного обращения к памяти для получения настоящего адреса функции в таблице, что замедляет выполнение, условно замедляет, потому что проц быстрый.
Во-вторых, для полиморфных объектов обычно требуется динамическое выделение памяти, поскольку размер объекта неизвестен на этапе компиляции. И если раньше разработчики игр практически всегда отключали rtti, то сейчас это норма, плюс нам пришлось его включить, когда мы начали использовать новую библиотеку для пользовательского интерфейса (WPFG) и её код стал пролезать по всей игре. В-третьих, виртуальные деструкторы усложняют управление памятью и могут привести к непредсказуемому поведению, но это отдельный случай.
Кто этот ваш CRTP вообще такой? CRTP - это паттерн программирования, при котором класс наследуется от шаблонного базового класса, передавая самого себя в качестве параметра шаблона.
Звучит сложно, но на практике это очень элегантное решение. Например: class Derived : public Base<Derived>
. Базовый класс может вызывать методы производного класса через static_cast, при этом все вызовы разрешаются на этапе компиляции. Идеально подходит когда мы знаем все возможные типы на этапе компиляции и хотим избавиться от вызова виртуальных функций. CRTP позволяет создавать шаблоны функций и классов, которые работают с любыми типами, реализующими определенный интерфейс, но без накладных расходов виртуальных функций.
template <typename Derived>
class GameObject {
public:
void update() {
// Вызываем метод из производного класса через static_cast
static_cast<Derived*>(this)->updateImpl();
}
void render() {
static_cast<Derived*>(this)->renderImpl();
}
};
class Player : public GameObject<Player> {
public:
void updateImpl() {
// Логика обновления игрока
// "Updating Player position and state\n";
}
void renderImpl() {
// "Rendering Player on screen\n";
}
};
... а еще посыпьте статическим полиморфизмом
Альтернативный подход — через рантайм полиморфизм с использованием std::variant, при котором выбор конкретной реализации метода происходит на этапе компиляции, а не во время выполнения программы. Компилятор заранее знает, какую функцию нужно вызвать, и генерирует прямой вызов без промежуточных обращений к таблицам или указателям, что полностью устраняет накладные расходы времени выполнения, связанные с полиморфизмом. Все работает так же быстро, как если бы вы напрямую вызывали нужную функцию, и при этом код остается гибким и расширяемым — можно легко добавлять новые типы и реализации без изменения существующего кода. Я уже частично рассматривал эту тему в Game++. Heap? Less
struct ButtonEvent {
int button_id;
void process() {} // "Обработка нажатия кнопки "
};
struct TimerEvent {
int timer_id;
void process() {} // "Обработка таймера"
};
struct NetworkEvent {
std::string message;
void process() {} // "Обработка сетевого события"
};
using Event = std::variant<ButtonEvent, TimerEvent, NetworkEvent>;
// Универсальный обработчик событий
struct EventProcessor {
template<typename T>
void operator()(T& event) const {
event.process();
}
};
Event e1 = ButtonEvent{42};
Event e2 = TimerEvent{7};
Event e3 = NetworkEvent{"Hello"};
std::visit(EventProcessor{}, e1);
std::visit(EventProcessor{}, e2);
std::visit(EventProcessor{}, e3);
... да промаринуйте в placement new и пулах
А еще часто нужно быстро создавать и уничтожать множество объектов — снаряды, эффекты, частицы, веревки и декали. Но вместо использования обычной динамической памяти будем применять статические пулы, что позволит перераспределять объекты без накладных расходов new/delete
и фрагментации памяти, что особенно важно на консолях и мобильных устройствах.
gtl::pool<Projectile, 64> projectile_pool;
auto* proj = projectile_pool.allocate();
// Настраиваем и используем снаряд
proj->velocity = . . .;
proj->damage = . . .;
projectile_pool.deallocate(proj);
Или иногда важно точно контролировать порядок и время инициализации объектов, похоже на пул, но не пул. Представьте что такой подход можно использовать для стартап аллокаций и создавать уникальные для игры объекты — конфиги, системы, менеджеры рендера, звука и т.д.
alignas(GameConfig) char _gameConfigStorage[sizeof(GameConfig)];
GameConfig* config = new(_gameConfigStorage) GameConfig();
GameConfig->load_from_file({. . .});
// После использования вызываем деструктор вручную
// или не вызываем вообще, потому что это объект уровня жизни всей игры
GameConfig->~GameConfig();
template<typename T>
class GameResource {
alignas(T) mutable uint8_t _data[sizeof(T)];
mutable T* _instance = nullptr;
public:
template<typename... Args>
T& init(Args&&... args) const
{
if (_instance) {
_instance->~T();
}
_instance = new (_data) T(std::forward<Args>(args)...);
return *instance;
}
void destroy() const {
if (_instance) {
_instance->~T();
_instance = nullptr;
}
}
T& ref() const {
assert(_instance);
return *_instance;
}
};
GameResource<GameConfig> g_config;
void Game::Init() {
. . .
g_config.init({100});
. . .
}
... и отправьте на сертификацию
Одна из главных причин почему мы вдруг стали так пристально на использование памяти — жалобы со стороны новых игроков и, внезапно, отказ в сертификации на тестах, что знаете‑ли не очень приятно и порождает резонные вопросы у руководства компании.
На этапе сертификации тестовая лаборатория Sony зафиксировала низкий фпс, коррапшены в памяти и деградацию производительности — ага, а мы думали они там просто в билд играют, и отказала в одобрении игры для публикации. Это стало неприятным сюрпризом для всей команды и пришлось пересматривать архитектуру памяти, использовать фиксированные аллокаторы и практически полностью избавляться от динамических выделений памяти. От всех, конечно, не избавились, но сейчас на кадре остались сотни аллокаций в главном потоке, а было их, чего уж греха таить, на порядок больше.
Почему продавец хотдогов никогда сам их не ест?
В моем случае — продавец хотдогов ест их сам и вынужден кормить ими всю команду, но мышки плачут и жалуются:) Используя C++, не стоит «принимать» использование кучи как данность — можно и нужно строить архитектуру так, чтобы не использовать динамическое выделение памяти во время выполнения. Плюсы все еще позволяют пользоваться преимуществами новых стандартов — лямбдами, RAII, шаблонами и даже новыми функциями C++23 — при этом сохраняя предсказуемость работы системы.
Правильный подход к архитектуре позволяет создавать надёжный код без жертв производительности. Работа без кучи не ограничивает возможности, а наоборот заставляет писать более чистый, предсказуемый и устойчивый код. У нас тут в игрострое предсказуемость очень часто важнее гибкости, поэтому проектируя свою систему, нужно заранее продумывать, где и как будет использоваться память.
Несмотря на очевидные технические преимущества, графики перфа, внутренние презентации, менторство — внедрение всего вышеописанного в реальных проектах часто сталкивается с сопротивлением команды. Многие привыкли к классическому ооп‑крестописанию с виртуальными функциями, наследованием и сахарным сахаром, который кажется им более интуитивным и понятным. Нескучное программирование требуют другого способа мышления о коде — нужно думать о типах, применении, шаблонной магии и вариантах объединения. И да, синтаксис становится более сложным, что поначалу может пугать и отталкивать.
К сожалению, практика показывает, что при первых же трудностях или дедлайнах команда быстро откатывается к знакомым привычкам. «Давайте лучше сделаем обычный интерфейс с виртуальными методами — это быстрее и все понимают» — не раз виденная автором реакция под давлением сроков.
Особенно сложно приживается подход в командах с большой текучкой кадров или аутсорсерами, которые не готовы тратить время на изучение кодстайла студии. В результатекод превращается в смешение стилей, что создает техническую неоднородность и в целом усложняет дальнейшее сопровождение проекта.
Мне интересно услышать мнение Хабражителей и разработчиков на C++ — кто из вас строит проекты без кучи? Какие приёмы и стратегии помогли вам сохранить читаемость и понимаемость кода, не жертвую современными возможностями языка? Сталкивались ли вы с необходимостью убеждать команду в использовании таких практик?
P. S. Телегу рекламировать не буду, её у меня просто нет:)
P.P.S Приходите на вебинар про оптимизацию в GameDev! Расскажу про кастомные аллокаторы, а еще обсудим с коллегами из игростроя и PVS‑Studio практические советы по улучшению проектов и способах ускорить запуск мобильных игр.
25 сентября в 16:00 (MSK)
https://pvs‑studio.ru/ru/webinar

Комментарии (0)
Jijiki
17.09.2025 16:02я вот сейчас тестирую кучу в дереве и это круче просто кучи :)
я не топ разраб, но получается можно построить 1 раз bvh(или 3 статика, динамика, интерфейс) динамический ограниченный по какому-то числу или параметру, и работать в рамках этой памяти наверно, еффекты получается тоже как-то туда портировать, вообще по возможности юзать распределенную память получается, тогда пол игры по памяти и столкновениям и каким-то мешам будут в дереве, в инете пишут это дерево для сверх быстрого поиска в дереве, тоесть если не куча то дерево получается, но я не прям супер клёвый специалист чтоб прям советовать это как альтернативу.
мелкие тесты
Скрытый текст
Генерация объектов: 1 мс Построение дерева: 182 мс hit Время поиска 1: 0 мс Test 1 (через сцену): hit Время поиска 2: 0 мс Обнаружено пересечений: 6 Время обхода: 0 мс std::vector<Node> nodes;//тоесть это внутри дерева for (int i = 0; i < 3000; ++i)//создаём 3000 тыщи AABB Tree() : nodeCount(0), rootIndex(nullIndex) { nodes.reserve(6000); //при этом дерево имеет свои размеры внутренние узлы+желаемое количество обьектов
просто вот просто на сухую говорить о пуле обьектов исключая из обсуждения пускай и возможно тайловую суть коллизий если игра 2д, там поидее красиво получается с деревом и она частично как мне кажется превратится в мемори-арена(просто потомучто в дереве будет весь функционал игровой)
ну и наверху по калбеку делать сбор из дерева, как бы да прям цикле, а что есть альтернативы(bind или functional+bind)
например не quicksort, а quickselect+kth туда же в копилку, а это разве не мемори-арена(мемори-арена это куча вроде удобная, но на 60 фпс могут быть промахи, соотв нужно 200 фпс для кешев), если дерево уже имеет всю арену выделенную
пс из приятного нету sqrt вызова есть просто сравнение границ :)
OlegMax
17.09.2025 16:02рантайм полиморфизм с использованием std::variant, при котором выбор конкретной реализации метода происходит на этапе компиляции, а не во время выполнения программы
Wat?! std::variant нужен в рантайме, оверхед я бы ожидал аналогичный вызовам виртуальных функций. Не понял, что у вас происходит в компайл тайм
dalerank Автор
17.09.2025 16:02Несложный вариант может развернуться в switch просто по индексам, первыми такую оптимизацию сделали разрабы кланга, потом подхватили остальные. У него внутри хранится индекс активного типа и сам объект, когда вызываеися visit стандартная реализация проходит через таблицу диспетчеризации, которая была сгенерирована на основе шаблонного кода. Почти все современные компиляторы (Clang и майки точно это делают) при включённых оптимизациях сворачивают это дело в обычный
switch
или даже jump-table по индексу. Зачемено на clang18+ если число вариантов не превышет 4.
Kelbon
17.09.2025 16:02Не знаю откуда у автора МИЛЛИСЕКУНДЫ на выделение памяти. Перестаньте уже демонизировать аллокатор. Он работает достаточно быстро, можете сделать бенчмарк и проверить, аллокация занимает около 30 наносекунд
Конечно если у вас горячий цикл, имеет смысл оттуда убрать аллокацию, вопрос - на что вы её замените. Будет ли это быстрее. Будет ли это поддерживаемо
Насчёт экономии памяти, т.е. тезис о том, что если вы пытаетесь использовать меньше кучу, то у вас программа ест меньше памяти - это ложный тезис. Наоборот, как только программа пытается меньше раз аллоцировать она начинает использовать буферы побольше, не деаллоцировать, а например складывать в фри лист и так далее. Это уже привозит и к фрагментации и к увеличению использования памяти в итоге. А самое глупое, что это приводит к тому что аллокация замедляется (сложнее аллоцировать, если много уже занято)
dalerank Автор
17.09.2025 16:02Исключительно практические примеры, 5к аллокаций на фрейме, слабый девайс уровня samsung a51 (средний фпс 42-45, 22-23ms), избавились примерно от половины аллокаций 2.5к-3к+ (средний фпс 50-55, 18-20ms). Ну т.е. это действительно миллисекунды на фрейм
Jijiki
17.09.2025 16:02а какие аллокации на фрейме появляются там надо просто взять адрес, частичек например, их же можно не выделять каждый раз, можно же выделить клиент 1 раз и обращаться к нему как к таблице по указателю или я что-то не понимаю? вот мы обсуждали как-то с вами, зачем создавать по новой сферу, взяли адрес примитива накинули цвет/размер, её адрес взят из таблицы например, воспользовались сферой убрали указатель, но она в таблице как примитив
ну и получается весь лего закинули в клиент наверно, после загрузки из таблицы берем лего куски и в рендер
Kelbon
17.09.2025 16:02Если вы смогли избавиться от половины операций которые делали на фрейме - неважно уже, были ли это операции аллокаций или какие-то другие. Просто начали меньше делать - получили больше fps
Если у вас аллокации кратковременные на фрейме, очевидная идея это сделать большой буфер, в котором можно без проблем быстрое выделять память, в конце фрейма считать что она очищена. Для долговременного использовать обычный newdalerank Автор
17.09.2025 16:02Ну собственно об этом и статья, если можно не делать, то можно не делать :) Но вопрос "где деньги, Зин"? Мы же не бенчмарки показываем продакту, а фпс с устройства и графики с бордов. Другой вопрос даст ли следующее выпиливание половины оставшихся такой же буст. Вот в том большом проекте мы остановились на пяти сотнях на кадр - профита мало, проблем много... ну их, пусть живут
Jijiki
17.09.2025 16:02а вы пробовали в майнкрафт поиграть на вашем таргет устройстве просто для сравнения - мини-бенчмарк такой? ну на телефоне можно еще top попытаться половить если вы свою приложуху запускаете, или может в самом андроиде есть диспетчер(если это апк )
сколько фпс на таргет устройстве в майнкрафте давайте тогда так ставить вопрос, ориентир 200 фпс не просто так произошел в наше время даже при всех прочих
если считать под 200 фпс минус просадка, но без промахов будет лучше если считать 60 с промахами
pavlushk0
17.09.2025 16:02И демонизировать виртульные методы, я бы добавил. Заметил тенденцию последних лет - все хаят традиционный рантайм полиморфизм, дескать там индерекшен, накладные расходы и т.д. (что конечно так и есть, но критично ли). И тянут (полезные но не всегда удобные) вариант и crtp. Посмотреть в исходники какого нибудь doom3, там virtual никого не смущало.
Kelbon
17.09.2025 16:02Не знаю откуда пошло поверье, что crtp хоть каким-то образом может заменить виртуальные функции. Это буквально инструмент для совсем другого. Да, согласен с вами в общем
Woodroof
17.09.2025 16:02Если это emdedded и система загружена, то легко могут быть и миллисекунды на одно выделение. Вполне такое видел на, скажем, головных устройствах не самых дешёвых машин.
AoD314
17.09.2025 16:02Добавлю только, что алокация становится еще немного медленнее когда у тебя есть много(16+) потоков, которые так же выделяют и освобождают память.
Kelbon
17.09.2025 16:02Если программист пытается добавить элемент в уже заполненный контейнер
это невозможно определить на компиляции. И почему-то совсем не упомянут факт, что ... не всегда известно максимальное число элементов на компиляции. Что тогда вы делаете?)
А std::function тоже умеет хранить объект на стеке, если он удовлетворяет некоторым условиям
немного CRTP
В традиционном ООП на плюсах мы часто используем виртуальные функции
CRTP не может заменить виртуальные функции. Это разные инструменты для совершенно разных задач.
dalerank Автор
17.09.2025 16:02да, не всегда это возможно, тогда мы переходим на pmr контейнеры, но сам факт (это исключительно я говорю про разработку игр) что вам пришлось вот посреди кадра выделять память через общий игровой аллокатор, значит гдето ошиблись с логикой. Нам не нужны тысячи и миллионы объектов, большая часть кода оперирует массивами не больше 128 элементов. Но есть и подсистемы, где массивы могут быть большими - и вот там уже нужно думать, как это обработать. Но повторюсь это относительно небольшой набор систем
vmx
17.09.2025 16:02Да, аллокации/деаллокации на куче влияют на производительность.
У проекта DPDK есть гайд по написанию высокопроизводительного кода. Не для геймдева, конечно, а для обработки сетевого трафика (там требования к производительности и надежности бывают еще жестче): https://doc.dpdk.org/guides-25.07/prog_guide/writing_efficient_code.html
Это гайд для более "традиционных" Intel/ARM, причем многопроцессорных, но, мне кажется, его интересно почитать даже просто для информации.
SadOcean
17.09.2025 16:02Не совсем понял, в чем профит CRTP
Типа мы вызываем обычныую функцию, а она - не ищет виртуальные методы, а делает вызов функции по ссылке?
Это разве не должно быть сопоставимо собственно с виртуальным методом?
То есть вообще даже не такой вопрос, а какой смысл?
Профит виртуальной функции в том, что разнородные объекты кладутся в список с типом базового класса, у них вызывается update - и он вызывается по разному у разных объектов.
Здесь, получается, общего интерфейса нет. После компиляции шаблона это будут разные методы Update.
То есть статически разрешится только если
Player player = new Player();
player.Update()
Но аналогично будет работать и если явно использовать и наследованный класс - компилятор в таком случае подставит сразу финальную реализацию, потому что знает тип.
Я, возможно, что-то не улавливаю, мой основной язык c# и плюсами я пользуюсь постолько поскольку.
azTotMD
17.09.2025 16:02Мне интересно услышать мнение Хабражителей и разработчиков на C++ — кто из вас строит проекты без кучи?
Почти не использую runtime аллокацию, стараюсь по максиму переиспользовать уже выделенную память. У меня правда проект вообще без графики, картинку отрисовывает уже браузерный клиент. Зато работает непрерывно уже 3 месяца.
Ivaneo
17.09.2025 16:02Используем на проекте комплексный подход.
На PS5 нам пришлось полностью отказаться от стандартного malloc. Время аллокаций и ожидания системного мьютекса внутри malloc + переключение контекстов было большой проблемой. Были вынуждены встроить хуки на вызовы malloc/free/realloc и написать свой системный аллокатор, для маленьких размеров использовали lock-free pool с фиксированным размером, pool allocator для средних (причем для каждого размера пул имеет свой мьютекс, что бы треды не вертелись на одном) и TLSF аллокатор для больших кусков > page size.
Для временных объектов по возможности используем inplace контейнеры с фиксированным размером (пример eastl::fixed_* контейнеры).Если размеры слишком большие для стека то используем арены и соотв аллокаторы к ним. В качестве memory resource арены используют линейный аллокатор (пример std::pmr::monotonic_buffer_resource).
Если объект переживает стек, но живет в течении фрейма, используется глобальный frame allocator который в качестве ресурса использует thread-safe lock-free линейный аллокатор. В начале каждого кадра ресурс обнуляется и память переиспользуется.Многие фичи используют по два уникальных pool аллокатора на каждую такую фичу (обычный + синхронизированный в зависимости от того изменяются ли контейнеры из разных потоков либо только в основном тике). Это обеспечивает локальность памяти, так что она не разбросана по всей куче а лежит в одной или соседних страницах. Аллокации этой фичи не делят между собой общие ресурсы (аллокаторы и их примитивы синхронизации) тем самым улучшая производительность и простоту отслеживания используемой памяти.
В местах где объектов одного типа очень много, используем object pool или его умную вариацию которая чуть медленнее, но умеет подчищать пустые чанки.
Отдельно стоит отметить page allocator который выделяет память кусками кратными page size напрямую у системы. В основном он используется как upstream resource для арен, пулов и пр.Плюс всякий сахарок что бы со всем этим было легко работать и не запутаться, отдельно написанная memory profiling тулза которая позволяет все это добро отслеживать, тюнинговать размеры, отлавливать места с множеством временных аллокаций у системы и пр.
dersoverflow
ну я, например.
использование mem_pool в 2008 году обеспечивало ускорение в десятки и СОТНИ раз: https://ders.by/cpp/mtprog/mtprog.html#3.1.1
а сейчас еще есть и off_pool: 32-битные смещения вместо указателей обеспечивают адресацию 64 гигабайт: https://ders.by/cpp/deque/deque.html#7
ahdenchik
Проблема с этим объяснением в том, что куча, доступная из C++, это тот же самый пул, но реализованный средствами libc