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

Динамическая память — это проблема в разработке игр
В играх и игровых движках, особенно на консолях и мобильных устройствах, управление памятью должно быть максимально предсказуемым. Это значит:
никакой неожиданной задержки из-за фрагментации кучи;
никакого риска падения 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});
. . .
}
... и отправьте на сертификацию
Одна из главных причин почему мы вдруг стали так пристально следить за использованием памяти — жалобы со стороны новых игроков и, внезапно, отказ в сертификации на тестах, что знаете‑ли не очень приятно и порождает резонные вопросы у руководства компании.
На этапе сертификации тестовая лаборатория вендора зафиксировала низкий фпс, коррапшены в памяти и деградацию производительности — ага, а мы думали они там просто в билд играют, и отказала в одобрении игры для публикации. Это стало неприятным сюрпризом для всей команды и пришлось пересматривать архитектуру памяти, использовать фиксированные аллокаторы и практически полностью избавляться от динамических выделений памяти. От всех, конечно, не избавились, но сейчас на кадре остались сотни аллокаций в главном потоке, а было их, чего уж греха таить, на порядок больше.
Почему продавец хотдогов никогда сам их не ест?
В моем случае — продавец хотдогов ест их сам и вынужден кормить ими всю команду, но мышки плачут и жалуются:) Используя 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 вызова есть просто сравнение границ :)

Notevil
17.09.2025 16:02Извините за оффтоп и за критику, но не могу больше терпеть.
Часто вижу ваши комментарии, пишите интересные вещи, вроде бы, и по делу, вроде бы. Но они часто заминусованные. Причина, как мне кажется, это ваш стиль написания. Каждый комментарий как пережеванная каша из слов. Нужно несколько раз прочитать и все равно нет гарантии понимания.
И мне обидно, что из-за этого ваше мнение, кажется, остается не услышанным.
Goron_Dekar
17.09.2025 16:02У человека выраженая дисграфия. Ну не может он писать лучше. Это особенности мышления. И вычитка не помогает. Знаю, потому что сам такой.
Тут неплохо помогает AI, но гнать через него каждый комментарий только чтобы не хватать минусцов - оно того не очень стоит.

Anarchon
17.09.2025 16:02У человека выраженная дисграфия.
Сомневаюсь, что к автору выше это заболевание имеет какое-то отношение, у заболевания есть четкие симптомы, а тут просто быстрый комментарий без вычитки. Вам диагноз доктор ставил, кстати?
Умение внятно выражать мысли в письменном виде - это обычный навык. Думаете все тут пишут внятные комментарии в потоке с первого раза?

Viacheslav01
17.09.2025 16:02Ну вот набивание мяча ногой тоже обычный навык, но сколько бы я не практиковал его в молодости больше 10 раз никак не выходило.

Anarchon
17.09.2025 16:02Сравнение просто отличное. При ударе по мячу ты можешь посмотреть на получившийся результат и поправить его столько раз, сколько нужно для успеха, да?
сколько бы я не практиковал
И сколько же, если конкретнее?

Viacheslav01
17.09.2025 16:02Сравнение отличное, оно просто в том, что не каждый навык можно отточить. Более того автор мог все это вычитывать не раз и уверен, что все просто прекрасно!
Ну лет 10, каждое лето )
Anarchon
17.09.2025 16:02Не каждый, иногда этому препятствуют заболевания. Но заболевания всегда имеют четкие признаки. Здесь их нет.
Позволю себе просто не верить.

Antohin
17.09.2025 16:02Есть такие люди, у кого мысли скачут вперед разговора. Надо принять и простить. При разговоре с заказчиками обычно предварительный созвон. Просто кидают несвязанными идеями, потом их приходиться формулировать, как я их услышал в письмо. Дальше уже начинается рабочий процесс

Manshoo
17.09.2025 16:02Закинул его коммент в дипсик и попросил объяснить. Он подробно мне всё разжевал, вот часть из его ответа
ИИ перевод by Deepsekk
Я сейчас экспериментирую со специальным деревом (BVH) для обработки столкновений в игре. Оно работает гораздо быстрее, чем просто хранение всех объектов в списке.
Я не топ-разработчик, но я понял, что можно один раз построить такое дерево (или несколько для разных типов объектов), выделить под него память заранее и потом очень быстро в ней работать. В это дерево можно поместить почти всё: столкновения, модели, эффекты. Тогда почти вся игра будет использовать этот быстрый поиск.
Я провел тесты: создал 3000 объектов. Построить дерево для них заняло 182 мс, но зато потом поиск всех столкновений занимает меньше 1 мс! Это невероятно быстро.
Главные плюсы: скорость и то, что для проверок столкновений не нужны сложные математические операции, только быстрые сравнения.
Объяснение как-то было получше, чем такой перевод))

Antohin
17.09.2025 16:02А знаете как больно такое читать в тикетах саппорта? 3+ линия техподдержки, меня вроде там и не должно быть как ведущего, но часто приходится впрягаться. Вот мне курсы Oracke по БД помогают? Нет, заветы пифий храма Оракула - надо догадаться в чем примерная проблема и вывести окольными путями собеседника на суть вопроса и додумать недостающие переменные

akuli
17.09.2025 16:02Если я правильно понял, вы по сути предлагаете объединить менеджер памяти с менеджером сцены. Для некоторых типов игр это может и сработать. Но для общего случая вряд ли

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.

Woodroof
17.09.2025 16:02Вы, случайно, с std::any не спутали?

dalerank Автор
17.09.2025 16:02неа, размер варинта всегла равен максимальному размеру из вариантов + мета (индекс, выравнивание, хеш), память всегда внутри самого варианта, реализация стандартизирована комитетом.
any - внутри хранится: указатель на объект + type_info. память в куче, кроме случаев с мелкой оптимизацией (SBO), реализация не стандартизирована комитетом и зависит от вендора. Или я не так вопрос понял?
Woodroof
17.09.2025 16:02Так я на оригинальный комментарий отвечал. Потому что я могу понять, почему определение типа в any сопоставимо с вызовом виртуальной функции, но не в случае variant'а.

vadvalskiy
17.09.2025 16:02это да но только не рекомендую использовать FastDelegate это сплошной horrible cast то есть через адрес памяти лежит что-то конкретное -> void* -> вернуть все что угодно, я например реализовал свой std::function и predicate (суть такова что это аналог lambda на этапе компиляции CRTP но ко всему прочему мы можем его наследовать так как обычная структура без сахарной ваты + реализация такова что может работать и в старых C++98 а ещё я заметил что старый стандарт как раз самый правильный в плане шаблонов он более строгий чем в C++11 те же horrible cast в шаблонах старый стандарт не позволяет) я даже move semantics на основе этого сделал в C++98

vadvalskiy
17.09.2025 16:02как правильно реализовать function и чтобы не было проблем, просто использовать TMP - Template Meta Programming

vadvalskiy
17.09.2025 16:02проблема C++ в том что его создал не визуализационер не тот кто видит как и что должно а тоо кто просто пиздит идеи других прикручивает абы как и всё а как по мне самый верный самый правильный стандарт C++ это как раз 98 ну единственным исключением может быть ток: ctor() = default/delete; template<Args...> noexcept constexpr но и тот под вопросом но это долго объяснять те же lambda не нужны так как у нас уже есть классы и структуры для решения любых задач и сложности создаёшь predicate а ещё первое правило ООП это то что все является объектом я сейчас как раз себе пишу базовую библиотеку она как раз основана на callable object's а ещё стал замечать то что C++ с новыми стандартами все больше нарушает как собственный базис так и базис C а самый основной базис C это то что как пишем так и читается как читается так и пишется

dalerank Автор
17.09.2025 16:02вы слишком категоричны в своих высказываниях, но доля правды есть :)

vadvalskiy
17.09.2025 16:02не и я тоже не говорю что это плохо или хорошо все что-то заимствуют но тут проблема куда шире взять C++98 он именно такой как бы C с классами где все пишется как и читается а вот начиная с C++11 уже идёт куда-то не туда те же lambda они уже выглядят как нечто не понятное и просто как обычная функция или объект который ты можешь прочитать там скрывается очень многое и интересное если надо могу выложить свой function (TMP) и predicate (TMP + CRTP) не хуже и той же std но одновременно с этим более гибкий для применения

Jijiki
17.09.2025 16:02прикол С++ в векторизации, но не просто на словах, а там кароче много тем на тему абстрактного доступа,тот кто создал язык знает эту особенность и конкретно её заметил, это мы можем не догадываться просто, что всё вокруг последовательностей-векторов
а вы знаете а на деле таки есть, С++ это интервальная арифметика
давайте для примера кину ссылку на обзор новейшей структуры фс для примера ZFS

akuli
17.09.2025 16:02В компайл-тайм происходит генерация кода для всех возможных вариантов, а в рантайме только выбор одного из заранее сгенерированных путей. Это не "статический полиморфизм" в чистом виде, но гораздо эффективнее классического динамического

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 раз и обращаться к нему как к таблице по указателю или я что-то не понимаю? вот мы обсуждали как-то с вами, зачем создавать по новой сферу, взяли адрес примитива накинули цвет/размер, её адрес взят из таблицы например, воспользовались сферой убрали указатель, но она в таблице как примитив
ну и получается весь лего закинули в клиент наверно, после загрузки из таблицы берем лего куски и в рендер

domix32
17.09.2025 16:02Собственно пулы и аллокаторы с резервом - как раз про что статья и говорит. Минус пулов конечно же, что не всякий объект будет переиспользован достаточно часто и останется висеть в памяти просто потому что.

Jijiki
17.09.2025 16:02предположим есть игра, с георазделами виртуальными о зонах которых знают разработчики, игрок видит бескрайний мир, тогда получается, коллизии, хотябы коллиззии доступны из клиента, и перед входом в зону произойдёт загрузка зоны
тогда мы видим следующее
Скрытый текст
// Проверка столкновений for (auto& o : objects) o.collided=false; for (auto &o : objects) { o.position += o.velocity*2.0f; if (o.position.x > 1000||o.position.x < -1000) o.velocity *= -1; if (o.position.y > 1000||o.position.y < -1000) o.velocity *= -1; if (o.position.z > 1000||o.position.z < -1000) o.velocity *= -1; std::vector<Object3D*> collisions; traverseBVH(bvhRoot, &o, collisions); for (auto &a : collisions) { a->velocity *= -1; } collisions.clear(); }тогда если уровень это поддерево(древа уровней) в нём уже есть этот вектор и там если мы видим только указатели, тогда нужны примеры выделений в фрейм тайме
потомучто указатель это 8 байт вроде
соотв оптимизировать можно выделить до входа в зону 300 коллизий и очищать эту сборку коллизий
тогда получается нагрузка ляжет на распределение данных при входе в зону, и обьектов которые будут менять свои состояния
что такое player - это система анимации, она уже готова(допустим есть готовые анимации на каждый чих, и они будут загружаться при входе в мир подобно входу в зону предварительной подготовкой)
хорошо, предположим enemy - тоже система анимаций и состояний, тут возможно и будет нагрузка, но тут на помощ приходит геометрия как предугадать какие айди enemy в view например и выбирать то будем тоже указатели
что такое частички - это система движений частичек, есть автомат частичек кидаем добиваемся еффекта, и выводим его в выделенный готовый фрагмент, соотв в обоих случаях мы просто проигрываем то на что указал указатель.
просто тогда нужны примеры какие именно выделения происходят в фрейм тайме

Jijiki
17.09.2025 16:02может и говорит, но нету конкретики примеров с выводом статистики, нет в конце концов условий конкретных почему именно надо выделять именно так(тоесть это надо наглядно показывать на игре прям моменты, где краши, где просадки фпс, с рендер доками, санитайзерами и прочее), при использовании разных стратегий плохих и хороших
вот например(просто пример) я вчера выделял 6000 в дерево, и top показал число 20T хотя по факту 28 мегабайт
тоесть не хватает для тематики линейных пуллов наверное proof of concept чтоб поставить точку почему именно так
fz3Td2zlgro например как тут

Kelbon
17.09.2025 16:02Если вы смогли избавиться от половины операций которые делали на фрейме - неважно уже, были ли это операции аллокаций или какие-то другие. Просто начали меньше делать - получили больше fps
Если у вас аллокации кратковременные на фрейме, очевидная идея это сделать большой буфер, в котором можно без проблем быстрое выделять память, в конце фрейма считать что она очищена. Для долговременного использовать обычный new
dalerank Автор
17.09.2025 16:02Ну собственно об этом и статья, если можно не делать, то можно не делать :) Но вопрос "где деньги, Зин"? Мы же не бенчмарки показываем продакту, а фпс с устройства и графики с бордов. Другой вопрос даст ли следующее выпиливание половины оставшихся такой же буст. Вот в том большом проекте мы остановились на пяти сотнях на кадр - профита мало, проблем много... ну их, пусть живут

Jijiki
17.09.2025 16:02а вы пробовали в майнкрафт поиграть на вашем таргет устройстве просто для сравнения - мини-бенчмарк такой? ну на телефоне можно еще top попытаться половить если вы свою приложуху запускаете, или может в самом андроиде есть диспетчер(если это апк )
сколько фпс на таргет устройстве в майнкрафте давайте тогда так ставить вопрос, ориентир 200 фпс не просто так произошел в наше время даже при всех прочих
если считать под 200 фпс минус просадка, но без промахов будет лучше если считать 60 с промахами

dalerank Автор
17.09.2025 16:02не очень понимаю ценность играть в майнкрафт на таргет девайсе и сравнивать их, не пойдешь же с этими данными потом к продакту. Идти надо со своими же графиками неделю, месяц, квартал назад

Jijiki
17.09.2025 16:02там именно в релизном варианте, сделана по уму работа с памятью, и отрисовкой большого количества чанков, и чанкование как оказалось может быть и линейным и не линейным за плату памятью(тоесть нужно еще погрузиться в сборщик мусора - в его структуру), сборщик мусора не пулл, и не линейный, соотв дальше все ситуации кладуться на такую родность памяти получается, ну и самое важное Майнкрафт тесно работает с памятью
соответственно на поверхности да просто пулл, но на деле не просто пулл даже если в фрейме будут нужны указатели
200 фреймсов дают хорошую локальность как мне кажется плюс ко всему даже с потерями от 200 будет лучше чем от 60 если отталкиваться
ну так выходит, что если укротить на 60 на гл например то локальность может упасть, и 200 как раз может поттянуть недостатки, а разве Майнкрафт на DX был в 2005?
Скрытый текст
// Обновление позиций кубов for (auto& c : cubes) { c.box.lowerBound += c.velocity; c.box.upperBound += c.velocity; for (int i=0; i<3; ++i) { if (c.box.lowerBound.x < -10 || c.box.upperBound.x > 10) c.velocity.x *= -1; if (c.box.lowerBound.y < -10 || c.box.upperBound.y > 10) c.velocity.y *= -1; if (c.box.lowerBound.z < -10 || c.box.upperBound.z > 10) c.velocity.z *= -1; } } //.............................................. { Stack stack; stack.Push(tree.rootIndex); // Ray ray = CreateRay(p1, p2); while (!stack.IsEmpty()) { int index = stack.Pop(); if (index == nullIndex) continue; const Node &node = tree.nodes[index]; for(int i=0;i<3;++i){ if (!(node.box.lowerBound.x < -10 || node.box.upperBound.x > 10)) continue; if (!(node.box.lowerBound.y < -10 || node.box.upperBound.y > 10)) continue; if (!(node.box.lowerBound.z < -10 || node.box.upperBound.z > 10)) continue; } if (node.isLeaf) { int objIdx = node.objectIndex; if (objIdx >= 0 && objIdx < (int)cubes.size()) { cubes[objIdx].box.lowerBound += cubes[objIdx].velocity; cubes[objIdx].box.upperBound += cubes[objIdx].velocity; for (int i=0; i<3; ++i) { if (cubes[objIdx].box.lowerBound.x < -10 || cubes[objIdx].box.upperBound.x > 10) cubes[objIdx].velocity.x *= -1; if (cubes[objIdx].box.lowerBound.y < -10 || cubes[objIdx].box.upperBound.y > 10) cubes[objIdx].velocity.y *= -1; if (cubes[objIdx].box.lowerBound.z < -10 || cubes[objIdx].box.upperBound.z > 10) cubes[objIdx].velocity.z *= -1; } } } else { stack.Push(node.child1); stack.Push(node.child2); } } }вот 2 примера 1 и того же и оба пулл имеют, на литкоде самая первая задачка частично к тому же вопросу, 2 цикла 1 последовательности еще может быть 3 пример
Bounding_volume_hierarchy тут кстати описаны случаи тоже немного

pavlushk0
17.09.2025 16:02И демонизировать виртульные методы, я бы добавил. Заметил тенденцию последних лет - все хаят традиционный рантайм полиморфизм, дескать там индерекшен, накладные расходы и т.д. (что конечно так и есть, но критично ли). И тянут (полезные но не всегда удобные) вариант и crtp. Посмотреть в исходники какого нибудь doom3, там virtual никого не смущало.

Kelbon
17.09.2025 16:02Не знаю откуда пошло поверье, что crtp хоть каким-то образом может заменить виртуальные функции. Это буквально инструмент для совсем другого. Да, согласен с вами в общем

equeim
17.09.2025 16:02Вариант это тоже рантайм полиморфизм, там просто количество возможных вариантов типа известно на этапе компиляции в любом месте где вариант используется. У него и открытого наследования разные области применения. Наследование нужно когда есть общий интерфейс поведения и функциям которые с ним работают пофиг на конкретную реализацию, например какой-нибудь логгер.
Вариант удобен когда нужно сложить несколько взаимоисключающих (от рантайм логики) значений (которые могут быть сами по себе никаким другим образом не связаны) в одном месте и затем как-то с ними работать. Например JsonValue который может быть либо числом, либо строкой, либо массивом/объектом и т.п. В данном случае у разных типов нет какого-то общего поведения но их надо просто хранить в одном месте (как значение ключа в объекте или элемент массива).

j4niwzis
17.09.2025 16:02Проблема virtual-ов не в накладных расходах на индерекшен. А в том, что компилятор не может легко выполнить инлайнинг. Инлайнинг это не просто "сэкономить немного на call-ах". Он открывает путь ко множеству оптимизаций. С std::visit же компилятор может раскрыть код в обычный switch, заинлайнить что ему нужно, а уже после производить дополнительные оптимизации.

pavlushk0
17.09.2025 16:02Инлайн это тоже "point", конечно же, но про indirection call я не просто так написал, посмотрите видео с любой конференция за последние лет 10, если там будут упомянуты виртуальные функции, то следом полетят камни в сторону vtable и indirection call.
Раскрою, немного, своё видиние пошире - C++ комьюнити полно "perfrmance" снобизма и безудержной тяги к велосипедам. Вот уже и рантайм полиморфизм в глубокой опале (доже вне геймдева и HFT), хотя его замена на visit (или что то ещё) это такая вплне себе оптимизация, а оптимизациии надо бы класть на какой то прочный (обоснованный) фундамент.

Woodroof
17.09.2025 16:02Если это emdedded и система загружена, то легко могут быть и миллисекунды на одно выделение. Вполне такое видел на, скажем, головных устройствах не самых дешёвых машин.

Viacheslav01
17.09.2025 16:0230 наносекунд аллокация занимает в пустой куче, а возьмите кучу, прогоните через нее 100500 циклов выделения/освобождения кусков в 100500 разных размеров. После этого уже делайте бенчмак, боюсь результаты могут вас неприятно удивить.

VADemon
17.09.2025 16:02можете сделать бенчмарк и проверить, аллокация занимает около 30 наносекунд
Удобно бенчмаркается, когда весь hot path аллокатора умещается в L1 кэш?

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# и плюсами я пользуюсь постолько поскольку.
ahabreader
17.09.2025 16:02Он просто позволяет задать интерфейс для статического (compile-time) полиморфизма, не прибегая к концептам как к слишком новой штуке.
То есть мы в такой шаблон функции
template<typename T> void foo(T bar) { bar.f(); }можем подавать* объекты любых классов, полагаясь на то, что у них есть метод f() (утиная типизация). А если нам это кажется небезопасным и хочется ограничить классы наследниками одного класса-интерфейса IBar, то это достигается с помощью CRTP.
template<typename TBarImpl> void foo(IBar<TBarImpl> bar) { bar.f(); }* или как там правильно будет, "неявно специализировать"?
___
Не раскрыта тема девиртуализации. Компилятор достаточно умён, чтобы иногда избегать виртуальных вызовов. Используем CRTP = отказались от динамического полиморфизма = у компилятора есть возможность для девиртуализации, закатывание солнца вручную через CRTP может оказаться избыточным.

dalerank Автор
17.09.2025 16:02делали внутренние тесты, компилятор конечно умный, но девиртуализирует совсем уж простые случаи, а у маек такое впечатление это вообще не работает

KakashiHatake32282
17.09.2025 16:02Поделитесь пожалуйста, из статьи не понял, как Вы у себя в проекте использовали CRTP? Ведь объекты потом не положишь в один контейнер потом (если не использовать еще наследование от Base класса), где и как их хранить тогда?

dalerank Автор
17.09.2025 16:02template<typename Missile> struct ProjectileBase { void move() { auto missile = static_cast<Missile*>(this); // рассчитываем перемещение по траектории } }; struct SimpleArrow : ProjectileBase<SimpleArrow>; struct FireStone : ProjectileBase<FireStone>;Например так, "исторически" стрелы и ядра были сделаны разными объектами и общего класса у них не было, оба класса использовали копипасту логики перемещения, немного различались при столкновениях. Лежат они в разных массивах

SadOcean
17.09.2025 16:02Так по идее если они лежат в разных массивах, то и виртуализации никакой?
Это по сути разные классы, которые связывает только Update
dalerank Автор
17.09.2025 16:02Их связывает общий тип и логика перемещения, по хорошему мы должны завести новый базовый тип и отнаследовать оба потомка от него, но придется решать связанные с этим проблемы, продумывать интерфейс и возможно чинить новые баги. Тут CRTP как один из примеров фикса такого легаси код

SadOcean
17.09.2025 16:02Я понял в чем идея
Думаю в Шарпах такое можно сделать шаблонами - ты собираешь классы с как бы наследованием (на самом деле они не имеют базового класса, только шаблон), и потом можешь их использовать в шаблонных же типах, к примеру коллекциях.
Код вызова и коллекций выглядит абсолютно одинаковым, но при компиляции для каждого типа данных получается свой тип коллекции и обработчики без виртуальных функций.
Ну или с минимумом (сами коллекции можно перебирать через базовый класс и таким образом для них вообще не нужно кода)
Соответственно для тысяч объектов не будет тысяч вызовов виртуальных методов.
Многие ECS построены по похожему принципу, в Rust богатые инструменты для этого тоже есть.
Как то вроде называется, какой то тип полиморфизма.

Kelbon
17.09.2025 16:02Это вообще не то как надо использовать CRTP. У вас вообще УБ в коде. Каст к базовому типу + вызов функции, которая вероятно внутри делает static_cast<TBarImpl&>(*this) == UB
И никакая "девиртуализация" невозможна с CRTP, хотя бы потому что CRTP ЭТО НЕ ВИРТУАЛИЗАЦИЯ. Что девиртуализировать, если виртуальных функций нет?
ahabreader
17.09.2025 16:02И никакая "девиртуализация" невозможна с CRTP, хотя бы потому что CRTP ЭТО НЕ ВИРТУАЛИЗАЦИЯ. Что девиртуализировать, если виртуальных функций нет?
А можете теперь извиниться за свою невнимательность? Я говорил о том, чтобы полагаться на девиртуализацию вместо CRTP.
Это вообще не то как надо использовать CRTP
Это опечатка, в примере надо передавать объект в функцию по ссылке, а не по значению, конечно.

Kelbon
17.09.2025 16:02Используем CRTP = отказались от динамического полиморфизма = у компилятора есть возможность для девиртуализации
здесь чётко для не знакомого с темой связывается CRTP и возможность девиртуализации. Куда ни глянь - принимают CRTP за альтернативу виртуальным функциям. Это нужно исправлять, на уровне всей отрасли уже распространилось
Это опечатка, в примере надо передавать объект в функцию по ссылке, а не по значению, конечно.
и это никак не исправит тот факт, что CRTP совсем не для такого использования. Если уже сделали шаблон - ну зачем вам IAbc там? Это же ничего не изменит. Только запутает читателя.
CRTP нужно исключительно для внутрянки в реализации шаблонной магии и более ни для чего. Это не нужно делать публичным интерфейсом, оно не является интерфейсом

ahabreader
17.09.2025 16:02здесь чётко для не знакомого с темой связывается CRTP
Здесь чётко обрезана цитата на удобном вам месте, дальше там "..., закатывание солнца вручную через CRTP может оказаться избыточным".
Куда ни глянь - принимают CRTP за альтернативу виртуальным функциям.
Да, в этой статье. В этом бенчмарке. Понятно, что с огромными ограничениями, потому что динамического полиморфизма нет.
Это не нужно делать публичным интерфейсом, оно не является интерфейсом
Но его используют в этом качестве.
Я не готов глубже обсуждать, потому что сам им почти не пользовался. Но мой взгляд на CRTP вот такой.

SadOcean
17.09.2025 16:02Ок, понял.
Я думал, что компилятор должен такие штуки уметь и с виртуальными методами уметь делать, если тип вызываемого объекта известен.
Но вообще наследование от шаблонов такое должно уметь - в этом случае динамического полиморфизма нет, просто методы по сути одинаково называются.

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 тулза которая позволяет все это добро отслеживать, тюнинговать размеры, отлавливать места с множеством временных аллокаций у системы и пр.

Jijiki
17.09.2025 16:02ну вот 3000(20 скучно, 100 мало, 3000-4000 самое то) кубов в виртуальном кубе, все двигаются, кубики разных размеров в рамках 10 например при столкновении с рамкой меняют направление и при столкновении друг с другом. пулл обьектов окей, но тогда самое первое будут вызовы sqrt(щас вот я по ААББ сужу вообще нету sqrt-только мин-макс и сравнение, и баланс дерева ),наверное, мемори пулл проще конечноже в этом случае и возможно даже двойной цикл, но есть нюансы же наверное, и вот мы пока получается рассматриваем только мемори-пулл и линейные аллокации, но есть же и другие типы реализаций, статики/динамики, да больше памяти, да запарно найти ктый елемент, но работает прикольно, тем более чем круче игровая приставка тем больше там памяти поидее
Скрытый текст

но тут пока без коллиззии всех со всеми, но суть в том что это могёт работать, я тестирую сейчас в онлайн компиляторе поновее реализацию
плюс avx если поддерживается на устройстве

lma10h
17.09.2025 16:02Когда я вижу такие статьи, то всегда думаю, чувак, где код, где бенчмарки ? Чтобы мы могли проверить, а не только на графики посмотреть. Иначе, это пока что "теория".
Да, какие-то профиты вы получили, не сомневаюсь, но какой ценой ? Но когда вы пишите, что в variant до 4 элементов будет switch, это же какой-то женский половой орган бро. А в общем случае что будете делать ? И сколько выигрыш.
Статья, в которой надо большими жирными буквами писать перед каждым абзацом "применять только после тщательного анализа тулзами", иначе вы учите плохому. Т.к. код усложняется х100, а профит в каждом отдельном случае еще неизвестен. Т.к. ребята в glibc и co, не тупые, если вы смотрели когда-нибудь код там (или чинили баги в этих бакетах и видели их оптимизации, там тоже есть хитрости).
Поймите меня правильно, есть ситуации когда лучше заранее выделить, и placement new, когда вообще не использовать полиморфизм, пулы и тп, т.е. вы хороший вопрос подняли.
Но, у вас это звучит прям как религия, а не оптимизация узких мест с доказательствами :)

JediPhilosopher
17.09.2025 16:02В геймдеве это стандартный и общепринятый подход. Просто за пределами геймдева он редко где востребован, буквально еще в паре отраслей.
Просто мало где за пределами геймдева рост времени работы какой‑то операции на одну миллисекнду критичен и заметен. А в геймдеве если у вас 60 фпс, то на один кадр у вас 16 мс на ВСЕ — на графику, игровую логику, физику. И выиграть 1 мс ценой адских усложнений кода вполне норм вариант, ведь разница между 16 и 17 мс на кадр это разница между «больше 60 фпс» и «меньше 60 фпс», а во втором случае при включенном vsync вы внезапно получите сразу 45 или вообще 30, так как там дискретные значения, поддерживаемые телевизором. А в мои времена (середина нулевых и PS3) просадка с 60 до 30 считалась прям плохим делом, а просадка ниже 30 вообще была запрещена — ваша игра не пройдет ревью от Sony и не будет допущена на платформу.
Вот и бились за миллисекнды изо всех сил. Ведь времени на обработку кадра никогда много не бывает, геймдизайнеры всегда найдут на что его потратить. Например патфайндинг поумнее сделать или физику подетальнее.
В общем в геймдеве это оправдано. В других сферах как правило нет, и выглядит очень странно. Особенно для ПО работающего с сетью или базами данных, где задержки сетей и дисков будут на порядки больше чем выигранные такими усилиями доли миллисекунд.

BoldDwarf
17.09.2025 16:02Не только в геймдеве.
В эмбеде тоже.
Если ты делаешь софт для допустим скоростной печати на конвейере, то у тебя может быть всего 25-30 миллисекунд на рендер и отсылку в хардвер печати, начиная от получения irq триггера печати.
Если аллокатор затупил - то все, продукт на конвейере испорчен.
А железо там как правило не сверхмощные x86 а arm и не самые мощние при этом.
Кроме того надо еще и UI какой-никакой рисовать.
Единственный вариант тут - не использовать никакие обращения к куче из подобных критических мест.
Мимо, человек который не раз искал потерянные еденицы миллисекунд.

wmlab
17.09.2025 16:02Интересное совпадение. Я как раз пытаюсь прикрутить к личному приложению (C# .NET 8) ArrayPool.Shared для избежания фрагментации - приложение выделяет и отдает сборщику мусора сотни тысяч мелких массивов. На долгих сеансах идет медленная утечка памяти (подозреваю фрагментацию). Пока резюме - рост производительности есть (с утечкой пока не разобрался), некоторые функции срабатывают за 6ms вместо 29ms, например. Но есть ограничения - можно арендовать только массивы степени двойки. То есть можно 512 или 1024 байт, но нельзя 768. Это очень неудобно, приходится ломать логику.

vvdev
17.09.2025 16:02У ArrayPool.Shared достаточно низкий лимит количества массивов в пуле.
Проверьте, возможно у вас одномоментно требуется сверх того и пул продолжает выделять и выбрасывать слишком много инстансов.Если так - стоит попробовать использовать ArrayPool.Create со своими настройками. Но он - ConfigurableArrayPool заметно медленнее в многопоточном high throughput. Нужно будет отбенчмаркать.
В моём случае переход со стандарного ConfigurableArrayPool на самописную адаптацию стандартного же SharedArrayPool дал очень ощутимый прирост. Но я микро и нано секунды ловлю, на уровне миллисекунд может и не заметно разницы в скорости.

force
17.09.2025 16:02То есть можно 512 или 1024 байт, но нельзя 768. Это очень неудобно, приходится ломать логику.
Потому что когда массивы степени двойки очень легко определить нужный бакет. Мы одной операцией (двоичный логарифм) находим его и дальше берём первый попавшийся элемент из массива. Если же нужны динамические размеры - то всё, мы резко начинаем терять на скорости поиска. Можно соптимизировать под конкретные случаи или же если используются не массивы, а объекты, то использовать пул объектов (например List<T> - очищаем, кладём в пул, забираем - с определённой вероятностью нам хватит уже выделенного места и не будет новых аллокаций, и это прямо очень просто реализуется).

mayorovp
17.09.2025 16:02Нет, там всё проще. Когда массивы ограниченного числа размеров - их можно организовать в пул. А вот если каждый массив своего особого размера - пул почти бесполезен.

rg_software
17.09.2025 16:02Спасибо за статью, интересно! У меня вопрос. Вот читаешь -- вроде сначала всё логично, выделение памяти, туда-сюда, статические буфера, вроде киваешь, соглашаешься. А потом выкатывается бомбический репцет -- свои вектора, свои практики -- память не выделять, держать всё в кастомных пулах и так далее. Почему сразу так всё сложно? Ведь в STL у каждого объекта может быть кастомный аллокатор. Казалось бы, напишите свой на основе статического буфера, и пользуйтесь std::vector на здоровье. И даже свой operator new можно сделать. Может, глобальная замена стандартного решения на 2-3 версии аллокатора для типичных случаев уже решит проблему, и огород незачем будет городить?

cdriper
17.09.2025 16:02все уже написали.
variant порождает switch-like код, который далеко не всегда будет эффективнее вызова функции по указателю.
гарантировать корректное использование вектора с ограниченным размером в compile-time невозможно. это сказки.

Panzerschrek
17.09.2025 16:02Использование контейнеров с встроенным хранилищем ограниченного размера на N элементов может быть проблематично с точки зрения использования кеша процессора. Если количество элементов меньше N, то оставшаяся память не используется, но висит мёртвым грузом и может засорять кеш. Куда лучшим мне кажется подход с аренным аллокатором, который используется на время кадра, а когда кадр подсчитан, вся выделенная память освобождается разом.

Viacheslav01
17.09.2025 16:02Кешу процессора фиолетово на размер вашего буффера, он будет тянуть линиями (CLS), и будет тянуть только запрашиваемые данные, если у вас буфер в килобайт, а работаете только с первыми 170 байтами, то кеш загребет толко N = (170 / CLS + 1) байт

Zara6502
17.09.2025 16:02В разработке для консолей (про мобильные устройства я молчу, потому что игра не влезает по памяти даже в восемь гигов)
а кто и на чем тогда играл в эту игру в 2000-е?

dalerank Автор
17.09.2025 16:02В 1999 году в игре было 13 цив, она занимала 60мб оперативки и 200мб на диске. Там не было аллокаций вообще, все работало на статической памяти. Игра пережила 3 переработки - 2007, 13 и 19 - сейчас там 53 цивы, 8к текстуры, 3д движок и все работало через new, объем пустой катки на двоих чуть меньше 5гб

Zara6502
17.09.2025 16:02Про сотни тысяч игроков я не приукрасил, ежедневное число игроков проекта моей команды частенько переваливает за 500к в стиме.
А у моей команды онлайн в стиме частенько переваливает за 500 миллионов. Я как тот дедушка из анекдота.

Fr3nzy
17.09.2025 16:02Не, ну кандидаты там есть. Разработчик Dota2, CS2, Apex, PUBG, Marvel Rivals, видимо.

Zara6502
17.09.2025 16:02в профиле 4A Games, но вопрос несколько в другом - в том месте где вы пишете о длине своего онлайна хорошо бы увидеть ссылку, это же элементарная вежливость.

dalerank Автор
17.09.2025 16:02КДПВ как бы намекает про что речь, извините, что не пишу название прямо - наши лигалы зарезали упоминания в статье
ссылка тут
https://steamdb.info/app/813780/charts/
Zara6502
17.09.2025 16:02спасибо.
КДПВ как бы намекает про что речь
мне эта картинка ни о чем не говорит.

Fr3nzy
17.09.2025 16:02500к в Стиме у нее никогда не было, справедливости ради. Мб суммарно с XBOX (не помню есть ли она там в Game Pass) и наберется столько

here-we-go-again
17.09.2025 16:02А зачем эпохе 2 (ну пускай даже с 4к артами переделанными) 16гб памяти и оптимизации рендера? Она же работала на 200 мгц целероне в свое время с 64гб памяти. Что вы там такое наворотили?

aamonster
17.09.2025 16:02Звучит история, мягко скажем, странно.
Такие вещи делаются не через "я отклоняю коммиты", а через единый гайдлайн для команды.
В большинстве случаев использование стандартной кучи вполне оправдано, но раз уж вы столкнулись с ситуацией, где она не подходит – то должно быть чётко расписано, что, как и почему. И проведено исследование, что именно лучше всего подходит для ваших задач. Далее пишется гайдлайн и см. пункт 1.

dalerank Автор
17.09.2025 16:02проблема в том, что все это прекращается ровно тот момент, когда человек написавший этот пропозал или гайд уходит из команды.

aamonster
17.09.2025 16:02Если гайд обоснован (в вашем случае выглядит обоснованным) – его форсит тимлид.
А если вы начинаете просто навязывать свой подход коллегам – коллектив избавляется от вас, и всё возвращается на старые рельсы.
Третий случай – когда коллеги с идеей согласны. Опять же, описываете её в гайдлайне, и его форсит как тимлид, так и коллектив в целом навязывает новичкам – как обезьяны из "здесь так принято".

dalerank Автор
17.09.2025 16:02на моем опыте (не очень релевантно, у меня не очень большая выборка команд) работал только первый кейс, если это не культивировать и не объяснять, то через какое-то время все возвращается к обычному программированию. Не могу не отметить удобство stl и возможность писать мало красивого кода. Про тимлида тут спорно, задача тимлида защищать команду от "лучей добра" и апрувать что вы там понаписали, на ревью он смотрит совсем уже грубые ошибки, а техлиды не у всех команд есть

Onni
17.09.2025 16:02Пришел к точно такому же подходу. Но не из страха аллокаторов, тут много плюсов:
Понимание ограничений игры. Сколько максимум будет объектов\событий в кадре.
Если все игровое состояние лежит в одной большой структуре, то его легко сохранить/восстановит просто скопировав память.
Подход с CRTP (не знал что так называется) или через tagged union просто на практике показался более удобным чем огромные списки наследования.

OlegZH
17.09.2025 16:02Простите, а можно задать вопрос дилетанту? (Может быть, придётся, однажды, и игры разработать. Если повезёт.)
1. А какой другой вариант?
2. Можно ли сразу взять большой кусок памяти и назначать конкретным объектам определённые фрагменты?
3. Почему C++? Может быть, имеет смысл создать какой-то специальный язык программирования для разработки игр? И если C++, зачем нужны библиотеки общего назначения, вроде STL, а не специализированные библиотеки?
Я (к сожалению) очень далёк от этой интересной области, но если бы я пытался делать игры, то полагался бы также и на стек, создавал бы батареи объектов в стеке, а куча нужна для особо «текучих» случаев. В принципе, её использование можно и ограничить. Но для этого надо задавать какие-то ограничения.

Jijiki
17.09.2025 16:02другой вариант это универсальное(наверно бинарное дерево наверно поиска) дерево где происходит баланс при вставке(как итог задача усложняется) и хорошо подумать над циркуляцией данных, тогда это частично (эта мегаструктура может покрыть почти всё потребление памяти при условии что всё из таблицы тянется - толстого клиента), потомучто в игре будут коллиззии
соответственно выделяем некий размер сразу под клиент, и в нём дерево/деревья с нормальной утилизацией вовремя данных(а это и есть как раз подход наоборот, не сортировка всей линейности а сортировка только того что надо, отрисовка органиченная, соотв уровни более чоткие более шаблонные, тоесть задача от пула координально наоборот выглядит при сравнении с линейными проходами до конца, и тоже есть минусы наверное)
соотв самый оптимал или делить пространство, или делать иерархии или миксить в универсальное, деля пространство (например рейкастинг в иерархию почти всегда нужен, потомучто надо отсечь то что не будет участвовать в коллизии, далее, статика просто стоит, все со всеми например, можно деревом, а можно конечно же и массивом, но массив 1 000 000 это больше проверок, чем поделить пространство и пробежаться по маленьким подпространствам, так что пулл, интересно, но в пулле должны быть деревья, а в дереве будет как раз пол игры)

aksis777
17.09.2025 16:02Привет из c#,
Threadpool+arraypool+span

UFO_01
17.09.2025 16:02span завезли и что-то похожее на arraypool вроде как тоже https://en.cppreference.com/w/cpp/memory/synchronized_pool_resource.html (извиняюсь если ошибся, не особо мастак c#)
А вот с многопоточностью у плюсов как-то не задалось, те же корутины пока что очень сырые. Вроде как в c++26 это обещают исправить и даже пулы потоков добавить, это который std::execution. Будем посмотреть.
0serg
17.09.2025 16:02С многопоточностью в C++ все что нужно есть со времен C++11. Просто в соответствии с принципами плюсов стандартная библиотека не пытается ни продвигать "единственно верный" подход (ибо их много), ни предлагать кучу возможных вариантов (зачем если можно взять стороннюю библиотеку под конкретно Ваши цели).

Nalivai
17.09.2025 16:02В нашем проклятом эмбедеде это всегда было стандартом, но мы рантайм аллокацию не любим не из-за накладных даже расходов, а из-за безопасности.

Sazonov
17.09.2025 16:02Раз уж вы говорите про цпп23, то там есть более человеко-читаемый подход к CRTP - через аргументы методов типа auto foo(this auto& self);

Aquahawk
17.09.2025 16:02Ох несколько больнее работа с аллокациями в unity. А вообще мало кто занимается на таком уровне производительностью в геймдеве.

Cheater
17.09.2025 16:02без накладных расходов виртуальных функций
Есть девиртуализация, именно для случаев когда можно догадаться на этапе компиляции куда именно в vtable пойдёт вызов вирт.функции. Вот пример из статьи, переписанный в виде вирт. функций, который компилятор смог девиртуализовать: https://godbolt.org/z/oszTM6nP7.

Cheater
17.09.2025 16:02Виноват, проглядел
callrax, девиртуализовались и заинлайнились только первые 2 вызова, третий нет.Очень странно почему 3й случай не девиртуализовался, в ряде случаев это работает (arm64 gcc https://godbolt.org/z/59osvf7b8), в других нет.

Flammmable
17.09.2025 16:02Истину глаголите! Подписываюсь под каждый словом!
В электронике есть агрессивная сета свидетелей динамической аллокации во встраиваемых системах, распространяющая скверну. Дискуссию не вывозят, но очень упёрты.

kirichenec
17.09.2025 16:02Sony зафиксировала низкий фпс, коррапшены в памяти и деградацию производительности — ага, а мы думали они там просто в билд играют, и отказала в одобрении игры для публикации
Всякие юбисофты, так понимаю, приносят сильно больше денег, поэтому к ним не относится?

dalerank Автор
17.09.2025 16:02можно попросить вейвер, т.е. отсрочку бага и выпустить как есть, но фиксить все равно придется. Про юбиков не знаю

klirichek
17.09.2025 16:02Если нужно динамическое определение объекта, а виртуализация кажется слишком дорогой - можно хранить явный указатель на функцию в мембере. Так по крайней мере на один лукап по памяти меньше: в классическом виртуальном объекте мы сперва лезем по его адресу, извлекаем адрес tvm, а потом идём в tvm и извлекаем адрес метода. А с указателем на функцию мы сразу извлекаем его, и никакого "избыточного слоя" tvm нет.

tenzink
17.09.2025 16:02Главная проблема виртуальности в потенциальной опасности, что компилятор не сможет заинлайнить код виртуальной функции и после провести убер-оптимизации. На этом фоне стоимость лукапа нужного указателя на функцию чуть меньше, чем ничто. С указателем на функцию способность компилятора провести "девиртуализацию" и встроить сделать инлайнинг стремительно уменьшается. Так что ваше предложение без дотошного бенчмарка конкретного сценаря - преждевременная пессимизация

klirichek
17.09.2025 16:02Это не проблема. В смысле, он в самом деле НЕ СМОЖЕТ ничего заинлайнить. Например, у вас есть тред-пулл и очередь задач (корутин). Там заведомо динамический список задач, задачи из списка придётся распознавать в рантайме, и вот в момент вызова использование указателя на функцию вместо виртуального вызова экономит этот самый лукап.

tenzink
17.09.2025 16:02В таких условиях (тред-пул и очередь задач) влияние ещё одного косвенного вызова увидеть нереально. Тут хоть сотню дополнительных виртуальных вызов вставь, будет тоже самое

akuli
17.09.2025 16:02Хорошо сформулировали мысль, которую многие чувствуют, но боятся сказать вслух: удобство современных фич C++ часто оплачивается непредсказуемостью поведения в рантайме. И в геймдеве, где бюджет кадра это закон, такая цена слишком высока

LostSense
17.09.2025 16:02Мда, вся статья нвписанна из-за того, что не способны написать пул и алокатор для stl. В системах рантайм такое используется, возмите на заметку.
А в целом, алокация через системный вызов нигде не обещена как O(1), а значит использовать просто так запрещанно

26rus_mri
17.09.2025 16:02Не копали ли вы в сторону собственных аллокаторов? Это естественно, что стандартный аллокатор не может идеально удовлетворить все потребности, он же как швейцарский нож - может всё, но всё не идеально. А можно сделать скальпель под себя. Без рассмотрения данного вопроса отказ от кучи может быть преждевременным.
Сам я не в гейм-дев, но к разрабатываемому ПО есть требование непрерывной работы на протяжении многих часов. На этапе запуска куча используется во всю, большими объемами и эти данные живут, пока ПО не выключат. Далее, в рабочем режиме, могут быть вызовы выделяющие небольшие объемы, которые вскоре освобождаются.
С описанными проблемами не сталкивался.

Aksyl
17.09.2025 16:02Хочу поделиться своим опытом разработки крупных игровых проектов на C++, где производительность и стабильность — это не просто приятные бонусы, а абсолютно естественные требования к разработке.
Дааа, видно как хорошо оптимизированы современные игры

RepppINTim
17.09.2025 16:02Подход здравый, но "я отклоняю комиты" звучит как рецепт для того, чтобы вас тихо ненавидела вся команда. Может было бы продуктивнее не запрещать, а предоставить удобные инструменты, которые поощряют правильный подход и делают его проще, чем неправильный?

dalerank Автор
17.09.2025 16:02Ну пока что мои доводы были достаточно убедительны, чтобы человек изменил решение и обычно я рядом пишу решение как можно сделать. Плюс по пятницам провожу в команде что-то вроде мастер классов, где обсуждаем ошибки и разные примеры, так что все эти решения на виду и не успевают забыться

bodyawm
17.09.2025 16:02Project Zomboid, написанная на Java с ее комплексными системами миграции и симуляции орд зомби, страдает от тормозов как раз из за аллоков на каждый чих. Итератор - аллок на хипе, класс, который вполне мог бы быть обычной структурой - аллок на хипе, даже простые операции типа перемножения матриц это либо аллоки, либо кэшированные в виде полей класса инстансы

Jijiki
17.09.2025 16:02а майнкрафт тоже лагает тот что на java? да системы разные зомбоид vs майнкрафт, но именно майнкрафт показывает работу с памятью как мне кажется, у майнкрафта есть несколько критических мест на которые мало кто обращает внимания

Aksyl
17.09.2025 16:02Майнкрафт лагает из-за нереально кривых рук разработчиков. Установка нормальных модов на оптимизацию (если надо могу дать исчерпывающую ссылку) поднимает фпс буквально на несколько сотен.

mayorovp
17.09.2025 16:02Да, майнкрафт лагает. Да, потому что на Java.

Siemargl
17.09.2025 16:02для Ява программ майнкрафт на удивление не лагает. вероятно там тоже арены итп
лагает, например, идея

mayorovp
17.09.2025 16:02Если обустроенный мир достаточно большой, и игрок перемещается по нему непрерывно и достаточно быстро (например, в вагонетке) - можно словить stop the world gc из-за всех загруженных, выгруженных, но не успевших собраться чанков.

Jijiki
17.09.2025 16:02наверняка можно, но ведь в яве есть очередь чанки там компактные, и координаты вагонетки известные, я клоню к тому что в очереди просто путь вагонетки по 3сняли 3(там на деле может и по 9 ну тоесть 256х256х256х9 игрок в центре, на выбраном максимуме, тоесть наверху) чанка добавили в очереди вперед поидее влезает
может как-то и проще ограничивают, а очереди на фоне отрабатывают
ну допустим вагонетка едет в 3 быстрее чем передвижение игрока, на фоне например мир отрисовался чанками, хм нет ну может быть гипотетически где-то есть попадания в гц, тут еще смотрим релизную, можно посмотреть еще как энтузиасты делают, ограничение можно отыграть от фрустума - он в определенный момент так или иначе будет нужен даже если на старте изучения, плюс есть колайды, надо посмотреть что можно сделать
еще зависит от механики добавления куба, убирания куба(если механика ексклюзивная, тоесть индивидуально обновляет это один момент, а может просто простримить чанк в память и мы даже не заметим, там память быстрая(типо сразу вектор памяти - чанк unmap-map в gpu)), получается если в 100 раз быстрее кликать попадаем в гц, но я не думаю что так будет

lgorSL
17.09.2025 16:02Итератор - аллок на хипе, класс, который вполне мог бы быть обычной структурой - аллок на хипе, даже простые операции типа перемножения матриц это либо аллоки, либо кэшированные в виде полей класса инстансы
Нет, современная JVM хорошо справляется с короткоживущими объектами. Например, если временный объект не покидает функцию, его на хипе можно не создавать. И если даже надо, то у каждого потока есть свой собственный кусочек памяти, когда он быстро и легко может насоздавать временных объектов.
Я бы наоборот сказал, что в java начинаются проблемы, когда разработчики вспоминают всякие устаревшие мифы про производительность и извращаются без какой-либо необходимости.
Примеры: сделать пул объектов. Ожидание - нет расходов на создание/удаление. Реальность - объекты постоянно занимают память, объекты раскиданы по памяти как попало, можно случайно использовать объекты два раза или забыть освободить объект в пуле и получить утечку в памяти. Вместо удобства java получается стрельба по ногам в стиле С++.
Кэшированные в виде полей класса инстансы: отвратительное решение. Во-первых будут весёлые баги при многопоточности, во-вторых JVM не может производить оптимизации и вынуждена реально писать/читать в этот объект в куче, потому что он доступен всем потокам. Самый треш, когда в libgdx в промежуточном вычислении используется вектор из трёх чисел и не покидает функцию, JVM могла бы его вообще не создавать, если бы не горе-оптимизаторы.Я написал физический движок на Scala с неизменяемыми объектами для векторов-кватернионов и прочего: https://github.com/Kright/ScalaGameMath/blob/master/pga3d/shared/src/main/scala/com/github/kright/pga3d/Pga3dMotor.scala
Думал, потому перепишу классы на изменяемые. Но сначала померял производительность. Оказалось, что вообще не нужно, и производительность такая же. Буквально в паре мест поправил после профайлера. Новые объекты для векторов буквально при каждой арифметической операции "создаются" - по факту хоть бы хны, работает быстро.
Вот демка с машинкой: https://www.youtube.com/watch?v=wqt0ylxBqnU
Шаг физики для десятка тел, сотня пружин и прочих связей, интегрирование методом Рунге-Кутты четвёртого порядка (т.е., с вычислением сил в четырёх точках на шаг) занимает 60 микросекунд. Я могу хоть 10к шагов в секунду сделать и при этом код очень просто написан.Я понимаю когда у людей есть необходимость типа недостаточной производительности и отдела тестирования Sony, но намного чаще, к сожалению, люди не понимают что делают и просто пишут кривой сложный код, который ни разу не быстрее, но намного хуже поддаётся рефакторингу и оптимизации.

bodyawm
17.09.2025 16:02Это касается Oracle JVM и может быть OpenJDK, а ведь есть еще и ведро и арт, по крайней мере раньше, легко мог фризнуть процесс на 16мс пока делает сборку мусора.
Когда я писал игру под совсем ретро гаджеты, кэширование полей и выкидывание явных итераторов давало большой буст к стабильности фреймрейта именно за счет уменьшения нагрузка на GC, но да, сейчас это не так актуально. До какого то определенного момента можно забить и делать 'тяп-ляп'.

lgorSL
17.09.2025 16:02Да. На самых первых андроидах байт-код вообще интерпретировался и скорость была ужасная. Да и потом, до ART был Dalvik и он тоже был медленным.
И были жёсткие ограничения по памяти (порядка десятков мегабайт на всё приложение).
И, к сожалению, рынок устройств у пользователей тоже на несколько лет отстаёт от новинок ОС/железа, поэтому разработчики извращались как могли, чтобы приложения хоть как-то работали сразу на всём.
И вдобавок java в Андроиде на кучу лет застряла на 6-7 версии (они давно говорят, что поддерживают java 8, но если например подсунуть байт-код третьей скалы собранный под 1.8, то окажется что поддержка не такая уж и полная, работать оно не будет).
И вот из тех дремучих времён идут всякие стереотипы и костыли.

bodyawm
17.09.2025 16:02Но вообще я согласен в какой то степени, TLAB, пока рейт аллокаций на кадр не очень большой, позволяет снизить цену аллоков чего то небольшого и в первую очередь промежуточных результатов вычислений

mayorovp
17.09.2025 16:02Нет, современная JVM хорошо справляется с короткоживущими объектами. Например, если временный объект не покидает функцию, его на хипе можно не создавать.
Увы, к итераторам это напрямую не относится, потому что итератор создаётся в методе
iterator()и возвращается из него. Чтобы данная оптимизация сработала - JIT должен сначала этот самыйiterator()заинлайнить, чему в свою очередь мешает виртуальность этого самого метода.Кстати, у ваших неизменяемых классов та же проблема.
И если даже надо, то у каждого потока есть свой собственный кусочек памяти, когда он быстро и легко может насоздавать временных объектов.
А вот эта штука и правда помогает.

Nanosek
17.09.2025 16:02Стандартные аллокаторы - это почти всегда медленно. Помню пришёл я в один проект и мне, как новичку, дали сразу задачу для испытание на прочность - программа лагала при каких-то там обстоятельствах. Я сразу подумал на память и просто заменил для теста стандартный malloc на tbb malloc и это уже дало огромный буст производительности. В итоге потом так и оставили его.

APh
17.09.2025 16:02В отличие от многих приложений - игры, особенно большие, часто работают часами без прерываний и должны поддерживать стабильный...
Да, полно приложений, которые должны часами работать, если не месяцами. На играх и Блокноте с Паинтом мир не заканчивается.

alexeishch
17.09.2025 16:02placement new - это штука которую я узнал когда делал сложную хобби-программу на esp32. Довольно интересно было узнать много нового, хотя я и не пишу на C++ на работе

PatakinVVV
17.09.2025 16:02Есть ли у вас автоматическая проверка “allocs per frame” в CI? У нас падает билд, если >0 на главном потоке

dalerank Автор
17.09.2025 16:02Выводить в ноль аллокации слишком дорого, да и нет смысла в zero allocation rule. Всему есть своя цена, и в определенный момент время разработчика становится дороже того профита что мы получаем.
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
unreal_undead2
Но этот стандартный пул сделан для общего случая - возможность выделять и освобождать куски произвольного размера в произвольные моменты времени. Для конкретной задачи (объекты фиксированного размера, известный паттерн использования и т.п.) можно сделать решение оптимальнее.
vadimr
В куче С++ серьёзная проблема в том, что указатели и ссылки в программе привязаны к конкретным адресам в куче, и это мешает, например, провести дефрагментацию свободного пространства или использовать ещё какие-то техники управления памятью, более продвинутые, чем "выделил и забыл до освобождения". В специально организованных пулах можно эту проблему преодолевать.
Antohin
Мы тут делаем тихий шажочек в сторону гарбаж-коллектор'ов, от чего всегда и хотели дистанцироваться создатели языка
dalerank Автор
Есть специальный вид аллокаторов которые возвращают дескрипторы, а не указатели
Nemoumbra
Но это добавляет слой индирекции, т.е. мало просто разыменовать, надо ещё и найти этот адрес по дескриптору.
al_shayda
Похоже на то. Но зачем же тогда плюсы использовать? В gamedev?
0serg
Везде где нужен soft real time․ У нас например это 3D scanner
dersoverflow
плюсы - это Скорость! в умелых руках.
а вообще, только с годами начинаешь понимать, что единственный ГЕНИАЛЬНЫЙ язык для РЕАЛЬНОГО применения - чистый С. точка.
Страуструп начал "С с классами". жаль, что не остановили...
ну а если к баранам, то нужен "чуть более С". но без дури.
к счастью, можно отбросить ссылки https://ders.by/cpp/norefs/norefs.html но это все-таки компромисс ;(
Moog_Prodigy
Ассемблер - это скорость в умелых руках!
Вот я например могу на ассемблере написать ногодрыг для светодиода и он будет мигать с частотой 1 мегагерц. Сможете на С реализовать хотя бы один мигающий пиксель экрана с той же частотой? /sarcasm
RepppINTim
Да, сейчас ускорение будет не в сотни раз, потому что системные аллокаторы сильно поумнели. Но, как показывает автор статьи, даже на современном железе выигрыш в миллисекундах на кадр все еще критичен
Antohin
Так вроде ответ очевиден - надо применять бытовую логику (мне тут говорили в соседнем топике, что она неприменима к хайттек проектам) и не использовать динамическую аллокацию к объектам создающимся/диспозящимся сотни раз в секунду