Pull to refresh

Comments 198

кто из вас строит проекты без кучи?

ну я, например.

использование 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

Проблема с этим объяснением в том, что куча, доступная из C++, это тот же самый пул, но реализованный средствами libc

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

В куче С++ серьёзная проблема в том, что указатели и ссылки в программе привязаны к конкретным адресам в куче, и это мешает, например, провести дефрагментацию свободного пространства или использовать ещё какие-то техники управления памятью, более продвинутые, чем "выделил и забыл до освобождения". В специально организованных пулах можно эту проблему преодолевать.

Мы тут делаем тихий шажочек в сторону гарбаж-коллектор'ов, от чего всегда и хотели дистанцироваться создатели языка

Есть специальный вид аллокаторов которые возвращают дескрипторы, а не указатели

Но это добавляет слой индирекции, т.е. мало просто разыменовать, надо ещё и найти этот адрес по дескриптору.

Иногда это может быть вполне приемлемой ценой

Ассоциативные контейнеры STL созданы недоумками!
Те же самые граждане написали и все остальное [в C++]

Похоже на то. Но зачем же тогда плюсы использовать? В gamedev?

Везде где нужен soft real time․ У нас например это 3D scanner

плюсы - это Скорость! в умелых руках.

а вообще, только с годами начинаешь понимать, что единственный ГЕНИАЛЬНЫЙ язык для РЕАЛЬНОГО применения - чистый С. точка.

Страуструп начал "С с классами". жаль, что не остановили...

ну а если к баранам, то нужен "чуть более С". но без дури.

к счастью, можно отбросить ссылки https://ders.by/cpp/norefs/norefs.html но это все-таки компромисс ;(

Ассемблер - это скорость в умелых руках!

Вот я например могу на ассемблере написать ногодрыг для светодиода и он будет мигать с частотой 1 мегагерц. Сможете на С реализовать хотя бы один мигающий пиксель экрана с той же частотой? /sarcasm

Делаем это на Haskell ;)

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

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

А ЧТО там тогда использовать? C? (потому что Java/C# пролетают - там таких вот фокусов не сделать. )

Ну я-то действительно о C подумал сразу. Но вообще-то плойка это Playstation наверное. И как-то asm у меня с геймдевом ассоциируется. А может, на GPU всяким таким заниматься "раз уж пошла такая пьянка"? HLSL тогда.
(А сами плюсы настолько тормознутые, что их и интерпретируемые языки вроде Perl 5.10 обгоняют.)

для удобной генерации пещер, да я знаю еще 1 способ на С более простой, но надо ведь в 1 карте :)

не С снизу, и всё равно этого примера не достаточно, потомучто на уровне должна быть 1 хейтмапа :) на С это очень скурпулезно или склеивать или генерировать в 1 куске

Скрытый текст

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

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

я вот сейчас тестирую кучу в дереве и это круче просто кучи :)

я не топ разраб, но получается можно построить 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 вызова есть просто сравнение границ :)

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

У человека выраженая дисграфия. Ну не может он писать лучше. Это особенности мышления. И вычитка не помогает. Знаю, потому что сам такой.

Тут неплохо помогает AI, но гнать через него каждый комментарий только чтобы не хватать минусцов - оно того не очень стоит.

У человека выраженная дисграфия.

Сомневаюсь, что к автору выше это заболевание имеет какое-то отношение, у заболевания есть четкие симптомы, а тут просто быстрый комментарий без вычитки. Вам диагноз доктор ставил, кстати?

Умение внятно выражать мысли в письменном виде - это обычный навык. Думаете все тут пишут внятные комментарии в потоке с первого раза?

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

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

сколько бы я не практиковал

И сколько же, если конкретнее?

Сравнение отличное, оно просто в том, что не каждый навык можно отточить. Более того автор мог все это вычитывать не раз и уверен, что все просто прекрасно!

Ну лет 10, каждое лето )

Не каждый, иногда этому препятствуют заболевания. Но заболевания всегда имеют четкие признаки. Здесь их нет.

Позволю себе просто не верить.

Тут и посты часто пишут так, что поймёт только человек с таким же заболеванием

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

А у него правда комментарии по делу?

Закинул его коммент в дипсик и попросил объяснить. Он подробно мне всё разжевал, вот часть из его ответа

ИИ перевод by Deepsekk

Я сейчас экспериментирую со специальным деревом (BVH) для обработки столкновений в игре. Оно работает гораздо быстрее, чем просто хранение всех объектов в списке.

Я не топ-разработчик, но я понял, что можно один раз построить такое дерево (или несколько для разных типов объектов), выделить под него память заранее и потом очень быстро в ней работать. В это дерево можно поместить почти всё: столкновения, модели, эффекты. Тогда почти вся игра будет использовать этот быстрый поиск.

Я провел тесты: создал 3000 объектов. Построить дерево для них заняло 182 мс, но зато потом поиск всех столкновений занимает меньше 1 мс! Это невероятно быстро.

Главные плюсы: скорость и то, что для проверок столкновений не нужны сложные математические операции, только быстрые сравнения.

Объяснение как-то было получше, чем такой перевод))

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

А тут аишка не помогает? Как раз её область деятельности на 99%, разобрать запрос и сделать его понятным.

ну если вы специалист и советуете пуллы, так покажите конкретные примеры с использованием библиотек, потомучто обсуждение нюансов выделения не отражает сути обсуждения всего конктекста игростроения, вы же не показываете сцену по каким-то причинам, стратегию описываете, нету proof of concept

выделения во фрейм тайме тоже не показываете приходится у вас спрашивать, визуализации проблемы нету

эти проблемы и без шаблонов можно рассматривать если архитектура удобная там будет удобно и без шаблонов на простой сцене

Так а нет общих рецептов - в какой-то момент (условно раз в неделю) открываем профайлер Pix, Tracy или Razor и смотрим сколько у нас аллокаций на кадре. Видим что их меньше условно 500, прикидываем худшее время на нашем железе, понимаем что все это занимает милисекунду или меньше, пробегаемся по явным местам которые можно убрать за час, если таких нет - то идем пилить таски. Практика показывает, что если старый код нельзя починить за час и он не очень сильно мешает, то чинить его будет дорого. На кодревью просим коллег не писать новые потенциально проблемные места или предоставить серьезную причину, что аллокация здесь нужна. Советовать пуЛ, аллокатор, статический массив и другие способы починить проблемное место надо понимания что вы хотите там получить. Примеры и сцена - подключитесь к проекту 0AD, какое-то время назад я наблюдал там очень печальную картину с выделением памяти, вот вам и примеры и поле для фиксов и реальный опыт. И, извините, я не очень понял этот вопрос, если это был вопрос.

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

в создании указателей например

    // Построение BVH
    std::vector<Object3D*> objPtrs;
    for (auto& o: objects) objPtrs.push_back(&o);
    BVHNode* bvhRoot = buildBVH(objPtrs);
//это пулл или не пулл?

тогда чем отличается ограниченный мир от ноды или конкретной ноды где игрок, даже если там есть и другие подсистемы - еввенты, музыка.

так да учитывая общий контекст и пуллов и игр не понятно о чем статьи, тоесть что вы хотите сказать в ваших статьях?

как например новичек поймёт где пулл, а где не пулл, как применить обзор ваш или проверить, если в игре или играх уже должны быть ускоряющие структуры по типу BVH/kd-tree/octotree/quadtree, нету даже медианы к чему пришли, на деле там всё сложнее же получается, тогда зачем обозревать пулл интересно уже

потомучто если пропустить 1 нюанс с выделением памяти пул обьектов превратится в проблему и выльется в неудобство архитектуры и рендеринга, тоесть есть минимум 2 пути, сразу создать указатели или если мы боремся с этим сталкиваемся в дальнейшем с архитектурными проблемами и обсуждаем бесконечно пулы обьектов

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

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

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

частично оно так и будет при учтении 1 момента, просто нода будет потолще, и сцена будет более выявленной, нет запроса же под открытый мир бесшовный

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

дажее ИИ делает такие ошибки :) и вот можно после такого созерцания поверить в пулы как раз

    plane.name = "Ground";
    plane.color = glm::vec3(0.2f, 0.8f, 0.2f); // зеленая
    objects.push_back(plane);
    // Создаем и позиционируем врагов
    for (int i = 0; i < 10; ++i) {
        Object enemy = cube; // копия
        enemy.position = glm::vec3(i * 2.0f - 10.0f, 0.5f, -5.0f);
        enemy.modelMatrix = glm::translate(glm::mat4(1.f), enemy.position);
  	    //std::cout << "Enemy_" + std::to_string(i) << std::endl;
        enemy.name = "Enemy_" + std::to_string(i);
        enemy.color = glm::vec3(1.0f, 0.0f, 0.0f); // красный враг
        objects.push_back(enemy);
        enemies.push_back(&objects.back()); // указатель
    }// сможете представить какие тут баги заложены? а это только 
1 из возможных зивков
Скрытый текст
Hit object:  at position -6, 0.5, -5
Hit object:  at position -4, 0.5, -5
Hit object:  at position -2, 0.5, -5
Hit object:  at position 0, 0.5, -5
Hit object: Enemy_6 at position 2, 0.5, -5
Hit object: Enemy_7 at position 4, 0.5, -5
Hit object: Enemy_8 at position 6, 0.5, -5
Hit object: Enemy_9 at position 8, 0.5, -5

ответ врятли сможете

эта маленькая ошибка отражается и на рендеринге как видим

а вот другой пример

    // Построение BVH
    std::vector<Object3D*> objPtrs;
    for (auto& o: objects) objPtrs.push_back(&o);
    BVHNode* bvhRoot = buildBVH(objPtrs);

если objPtrs не уходит дальше локального стека, зачем вы делаете его через вектор?

https://godbolt.org/z/q6Pzn5qvz вот пофикшено как мне кажется то что я хотел показать, хотя первый пример по расстоянию тоже работает, но тут внутри полость и куб выражен ограничителями сборными

теперь ради примера можно добавить такие деревья в дерево и сталкивать большие кубы, а в больших кубах сталкивать маленькие

рантайм полиморфизм с использованием std::variant, при котором выбор конкретной реализации метода происходит на этапе компиляции, а не во время выполнения программы

Wat?! std::variant нужен в рантайме, оверхед я бы ожидал аналогичный вызовам виртуальных функций. Не понял, что у вас происходит в компайл тайм

Несложный вариант может развернуться в switch просто по индексам, первыми такую оптимизацию сделали разрабы кланга, потом подхватили остальные. У него внутри хранится индекс активного типа и сам объект, когда вызываеися visit стандартная реализация проходит через таблицу диспетчеризации, которая была сгенерирована на основе шаблонного кода. Почти все современные компиляторы (Clang и майки точно это делают) при включённых оптимизациях сворачивают это дело в обычный switch или даже jump-table по индексу. Зачемено на clang18+ если число вариантов не превышет 4.

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

неа, размер варинта всегла равен максимальному размеру из вариантов + мета (индекс, выравнивание, хеш), память всегда внутри самого варианта, реализация стандартизирована комитетом.
any - внутри хранится: указатель на объект + type_info. память в куче, кроме случаев с мелкой оптимизацией (SBO), реализация не стандартизирована комитетом и зависит от вендора. Или я не так вопрос понял?

Так я на оригинальный комментарий отвечал. Потому что я могу понять, почему определение типа в any сопоставимо с вызовом виртуальной функции, но не в случае variant'а.

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

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

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

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

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

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

а вы знаете а на деле таки есть, С++ это интервальная арифметика

давайте для примера кину ссылку на обзор новейшей структуры фс для примера ZFS

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

Не знаю откуда у автора МИЛЛИСЕКУНДЫ на выделение памяти. Перестаньте уже демонизировать аллокатор. Он работает достаточно быстро, можете сделать бенчмарк и проверить, аллокация занимает около 30 наносекунд

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

Насчёт экономии памяти, т.е. тезис о том, что если вы пытаетесь использовать меньше кучу, то у вас программа ест меньше памяти - это ложный тезис. Наоборот, как только программа пытается меньше раз аллоцировать она начинает использовать буферы побольше, не деаллоцировать, а например складывать в фри лист и так далее. Это уже привозит и к фрагментации и к увеличению использования памяти в итоге. А самое глупое, что это приводит к тому что аллокация замедляется (сложнее аллоцировать, если много уже занято)

Исключительно практические примеры, 5к аллокаций на фрейме, слабый девайс уровня samsung a51 (средний фпс 42-45, 22-23ms), избавились примерно от половины аллокаций 2.5к-3к+ (средний фпс 50-55, 18-20ms). Ну т.е. это действительно миллисекунды на фрейм

а какие аллокации на фрейме появляются там надо просто взять адрес, частичек например, их же можно не выделять каждый раз, можно же выделить клиент 1 раз и обращаться к нему как к таблице по указателю или я что-то не понимаю? вот мы обсуждали как-то с вами, зачем создавать по новой сферу, взяли адрес примитива накинули цвет/размер, её адрес взят из таблицы например, воспользовались сферой убрали указатель, но она в таблице как примитив

ну и получается весь лего закинули в клиент наверно, после загрузки из таблицы берем лего куски и в рендер

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

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

тогда мы видим следующее

Скрытый текст
        // Проверка столкновений
        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 например и выбирать то будем тоже указатели

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

просто тогда нужны примеры какие именно выделения происходят в фрейм тайме

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

вот например(просто пример) я вчера выделял 6000 в дерево, и top показал число 20T хотя по факту 28 мегабайт

тоесть не хватает для тематики линейных пуллов наверное proof of concept чтоб поставить точку почему именно так

fz3Td2zlgro например как тут

например вот

тот же цикл только пофикшен подход(position-based + фикс выхода за границы) и выход за границы(я их пока не добавил как plane-обьекты в дерево) -O3 sse avx 9%(на сухую 35%) 28 мегабайт 2000 обьектов дерево, еще кстати такой момент хотел добавить к теме пулов под игру, хорошо имеем пул, а разве у пула не должно быть менеджера памяти по аналогии с сборщиком мусора, даже учитывая что используется надежда на С++ стратегию очищении при выходе из региона

и тогда если есть какой либо менеджер памяти линейного пускай пулла под игру, выглядит это так

memoryManagerInsert(Cube);
memoryManagerInsert(Camera);
memoryManagerInsert(Terrain);
memoryManagerInsert(House);
memoryManagerInsert(Player);
prepareSubtreeLikeTreeOnLevel<objs*> = loadLevel();
//получаем список(или дерево указателей на родителей и количество) на 
уровне наверное или фиксируем в дереве уровня
в момент загрузки видим загрузку (бар, потомучто надо 
зарегестрировать ресурсы в менеджере как родитель обьекта
и соорудить список указателей на уровне)

и получается на фоне он проверяет используется ли ресурс или повис без использования

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

Если у вас аллокации кратковременные на фрейме, очевидная идея это сделать большой буфер, в котором можно без проблем быстрое выделять память, в конце фрейма считать что она очищена. Для долговременного использовать обычный new

Ну собственно об этом и статья, если можно не делать, то можно не делать :) Но вопрос "где деньги, Зин"? Мы же не бенчмарки показываем продакту, а фпс с устройства и графики с бордов. Другой вопрос даст ли следующее выпиливание половины оставшихся такой же буст. Вот в том большом проекте мы остановились на пяти сотнях на кадр - профита мало, проблем много... ну их, пусть живут

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

сколько фпс на таргет устройстве в майнкрафте давайте тогда так ставить вопрос, ориентир 200 фпс не просто так произошел в наше время даже при всех прочих

если считать под 200 фпс минус просадка, но без промахов будет лучше если считать 60 с промахами

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

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

соответственно на поверхности да просто пулл, но на деле не просто пулл даже если в фрейме будут нужны указатели

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 тут кстати описаны случаи тоже немного

Ключевые слова – "на фрейме". Тех, кто привык разумнл пользоваться кучей, это и правда шокирует.

Ага, и я боюсь с приходом НС разумных станет еще меньше.

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

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

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

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

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

Инлайн это тоже "point", конечно же, но про indirection call я не просто так написал, посмотрите видео с любой конференция за последние лет 10, если там будут упомянуты виртуальные функции, то следом полетят камни в сторону vtable и indirection call.

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

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

так тут же про геймдев вроде как..

так может у него playdate какой-нибудь.

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

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

это грубый пример хаотичное выделение как нагрузка, в игре можно что-то подготовить же

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

и покажу +- пример какой получился у меня(напомню зоны статичные внутри зон могут быть и статики и динамики)

Скрытый текст

ниже типо стресс тест

Так выглядит внутри активной зоны. Здесь нагенерено 800 000 кубов по 100 000 на каждую из 8, зеленый куб для дебага(механика телепортации и привязки - потипу как спайдер мен только привязываемся к кубику)
Так выглядит внутри активной зоны. Здесь нагенерено 800 000 кубов по 100 000 на каждую из 8, зеленый куб для дебага(механика телепортации и привязки - потипу как спайдер мен только привязываемся к кубику)

мы же не будем на 1 пуле держать всю игру или игровой-локационный(внутри кучки)(а сами регионы тоже регионы)-гео-регион

Могли бы вы переформулировать предложение? Я не очень понял о чем речь идет.

а какой структурой вы пользуетесь есть ли физика у вас? как вы пулы организовываете, как организуете структуры(географические) где игрок может делать интерактив? тоже не достаточно контекста

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

или просто из ускоренной структуры убрать указатель что приведёт к перестройке локального дерева пака мобов

тоесть да, есть вариант удалять, но с ускоренной структурой и гео-спатингом можно подумать с корзинами и свайпом обьектов, чтобы не перевыделять, а импорт/експорт по мере пропадания

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

Физика есть, но она в стражках специфичная, пользовались разными, Havok -> Box2D -> Jolt -> потом ушли на свое решение поверх box2d -> потом опять вернулись к Jolt. В других проектах обычно PhysX/Bulllet

я хочу сказать, что всё что стоит на поверхности выделенной каким-то(методом) для передвижения, будь то домик или пачка квестовых мобов с точки зрения региона, это подобьемы внутри региона как не крути, если нужна оптимизация и если не надо искать(эти координаты даже если они как в блендере относительные они привязаны к мировым), и получается если сделать, по ограничениям каким-то(там по разному) это практически от 1 евента или 1 проверки, по типу где игрок, а игрок в регионе 1 например, а значит рядом с ним, сегменты такие-то и их не будет много, потомучто масштаб - полигоны - навесные конструкции/мобы/отсекатели входов в пещеру так же можно соорудить, на входе обьем, вошли в обьем(и timestamp) всё за поворотом можно не рисовать что сзади

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

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

Опять не очень понятно, что вы хотите спросить. Если вы под "подобъемами" подразумеваете окто- и квадродеревья или другие виды spatial разбиения пространства, региона как вы выразились, то да таких методов достаточно много и они давно и хорошо описаны в технической литературе и применяются повсеместно. Могу посоветовать для начала Real-Time Collision Detection Кристофера Эриксона, там подробоно все разобрано. Еше есть Artificial Intelligence for Games Миллигтона, она довольно скучная и с кучей теории, но там тоже все подробно изложено.

тогда о какой дороговизне пулов(и перевыделений) мы говорим если вы всё это сами знаете

причем box2d вроде делал обзор или не он, но обзор есть, вот можно даже игру по-лучше чем у сиплюсплюсгай сварганить, без удаления коллизии - потомучто её просто не будет в той точке

а это всё из-за того нюанса при выделении как попали указатели обьектов в деревья

или в таблицу деревьев уровня

у этой книги база хорошая, но на деле даже не знаю, по поводу бвх я не пробовал с этой книги, я делал по другому гайду :)

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

тоесть граф регионов(а регион это 1 большой обьем где регион), и получается логика выстраивается есть большая карта, углубились в квадратик, там хейтмапа настроена на оптимал, выбрали высоты или выбрали пути, простроили граф и получается и граф и бвх

ну дорого да, будет 4 гигабайта(и база данных на клиенте), а как еще и ходить и летать и взхаимодействовать вот интересно, опуская проблему пула даже

вот пример есть террейн, на нём группа-пак созданий-кубов, зачем искать абстракции если это локальное дерево группируемое кубиком, и этот кубик уже дерево и имеет координаты(тоесть кубический ограничитель специальный простроен не из плоскостей а из ААББ, а кубики создания уже в нём, и эта нода уже на координатах кубических стен, тоесть эти создания снизу ограничены террейном, а по краям чтобы вернуться обратно в отряд стенками ААББ-которые имеют маленький обьём каждая) и это прикольно работает даже без OBB, но наверно и ОББ можно прикрутить

а вот еще в копилку почему обьемы удобнее

Скрытый текст

смотрите регион в обьеме и не надо ничего искать всё можно организовать для поверхности в обьеме

это вид с уголка как минимум работа со светом упрощается хотябы по позиционированию

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

Лучше если Вы будете давать более точные термины в своих ответах, тогда можно это обсуждать с одинаковым их пониманием, общепринятые (когда речь идёт о делении сцены/мира) - регионы (octree, quadtree, grid, BVH, BSP). Сейчас несколько неточно использовано словосочетание "регион в объеме", оно наверное что-то значит, но при таком написании совершенно теряется технический смысл. На сцене есть регионы, в регионе существуют кластеры - это группы чанков или секторов, чанк разбивается на ячейку (cell, если это навмеш или другая сетка) и нода (если это граф или непериодческая структура). Теперь вопрос, в каких терминах вы хотите описать эту зеленую картинку? Прочтите, если будет возможность, Эриксона, там это все объяснено очень доступно

да можно и так называть, тогда ваши статьи не годятся к обзору пула, потомучто технически не точно описывается что вы имеете ввиду и не представлена суть, так сказать подводящая к пулу конкретно в игре - пример, потомучто этих пулов больше по контекстам, чем просто перечисление

потомучто мы обсуждаем по статье пул как последовательность, а там не совсем так как мы видим с вами

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

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

тут даже дело не в к мерности, а в констатации факта что без обьема нереально просто

тогда можно обсуждать стек на векторе, а зачем, если написав эти 6 строчек он делает что нужно

потом мы смотрели что надо делать резерв, а для пула под дерево так не надо делать, в дерево например по 1 елементу добавляется, и не важно какая там скорость

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

Скрытый текст
struct Object3D {
  glm::vec3 position;
  glm::vec3 prevpos;
  glm::vec3 velocity;
  float spped;
  glm::vec3 size;     // для кубика
  glm::vec3 radius;   // для сферы
  bool isSphere;
  bool collided = false; // для визуализации столкновений
  bool stayObj = false;
  glm::vec3 NormalWall;//(1, 0, 0)
  std::string name;
};


struct BVHNode {
  AABB box;
  BVHNode* left = nullptr;
  BVHNode* right = nullptr;
  std::vector<Object3D*> objects; // листовой узел
  bool isLeaf() const { return !objects.empty(); }
  ~BVHNode() {
    delete left;
    delete right;
  }
};

BVHNode* buildBVH(std::vector<Object3D*>& objs, int depth=0) {
  BVHNode* node = new BVHNode();
  // Создаем изначальный AABB
  node->box = AABB(glm::vec3(FLT_MAX), glm::vec3(-FLT_MAX));
  for (auto* o : objs) node->box.expand(*o);
  if (objs.size() <= 2) {
    node->objects = objs;
    return node;
  }
  int axis = depth % 3;
  auto compareAxis = [axis](Object3D* a, Object3D* b) {
    float aCoord = (axis==0) ? a->position.x : (axis==1) ? a->position.y : a->position.z;
    float bCoord = (axis==0) ? b->position.x : (axis==1) ? b->position.y : b->position.z;
    return aCoord < bCoord;
  };
  std::sort(objs.begin(), objs.end(), compareAxis);
  size_t mid = objs.size()/2;
  std::vector<Object3D *> leftObjs(objs.begin(), objs.begin()+mid);
  std::vector<Object3D *> rightObjs(objs.begin() + mid, objs.end());
  //std::cout<<depth<<std::endl;
  node->left = buildBVH(leftObjs, depth+1);
  node->right = buildBVH(rightObjs, depth+1);
  return node;
}

и тут получается совмещение, но заполнение инкрементал, а пул отделен обьектами, а в дереве просто указатели и это не так как с индексами, тут минус в том, что нету BB(есть только точки), но это меньше памяти и оптимально пока еще для 1024х1024 куска по трем осям

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

просто 2 этапа делал на сухую - не понимал, что не так

попробовал дерево с ограничивающими обьемами всё пошло как по маслу хоть уровень конструируй

можете сделать бенчмарк и проверить, аллокация занимает около 30 наносекунд

Удобно бенчмаркается, когда весь hot path аллокатора умещается в L1 кэш?

Фикс-vector и арены реально спасают

Добавлю только, что алокация становится еще немного медленнее когда у тебя есть много(16+) потоков, которые так же выделяют и освобождают память.

Если программист пытается добавить элемент в уже заполненный контейнер

это невозможно определить на компиляции. И почему-то совсем не упомянут факт, что ... не всегда известно максимальное число элементов на компиляции. Что тогда вы делаете?)

А std::function тоже умеет хранить объект на стеке, если он удовлетворяет некоторым условиям

немного CRTP

В традиционном ООП на плюсах мы часто используем виртуальные функции

CRTP не может заменить виртуальные функции. Это разные инструменты для совершенно разных задач.

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

Да, аллокации/деаллокации на куче влияют на производительность.

У проекта DPDK есть гайд по написанию высокопроизводительного кода. Не для геймдева, конечно, а для обработки сетевого трафика (там требования к производительности и надежности бывают еще жестче): https://doc.dpdk.org/guides-25.07/prog_guide/writing_efficient_code.html

Это гайд для более "традиционных" Intel/ARM, причем многопроцессорных, но, мне кажется, его интересно почитать даже просто для информации.

Не совсем понял, в чем профит CRTP 
Типа мы вызываем обычныую функцию, а она - не ищет виртуальные методы, а делает вызов функции по ссылке?

Это разве не должно быть сопоставимо собственно с виртуальным методом?
То есть вообще даже не такой вопрос, а какой смысл?
Профит виртуальной функции в том, что разнородные объекты кладутся в список с типом базового класса, у них вызывается update - и он вызывается по разному у разных объектов.
Здесь, получается, общего интерфейса нет. После компиляции шаблона это будут разные методы Update.
То есть статически разрешится только если
Player player = new Player();
player.Update()
Но аналогично будет работать и если явно использовать и наследованный класс - компилятор в таком случае подставит сразу финальную реализацию, потому что знает тип.

Я, возможно, что-то не улавливаю, мой основной язык c# и плюсами я пользуюсь постолько поскольку.

Он просто позволяет задать интерфейс для статического (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 может оказаться избыточным.

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

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

template<typename Missile>
struct ProjectileBase  {
    void move() {
        auto missile = static_cast<Missile*>(this);
        // рассчитываем перемещение по траектории
    }
};

struct SimpleArrow : ProjectileBase<SimpleArrow>;
struct FireStone : ProjectileBase<FireStone>;

Например так, "исторически" стрелы и ядра были сделаны разными объектами и общего класса у них не было, оба класса использовали копипасту логики перемещения, немного различались при столкновениях. Лежат они в разных массивах

Так по идее если они лежат в разных массивах, то и виртуализации никакой?
Это по сути разные классы, которые связывает только Update

Их связывает общий тип и логика перемещения, по хорошему мы должны завести новый базовый тип и отнаследовать оба потомка от него, но придется решать связанные с этим проблемы, продумывать интерфейс и возможно чинить новые баги. Тут CRTP как один из примеров фикса такого легаси код

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

Многие ECS построены по похожему принципу, в Rust богатые инструменты для этого тоже есть.
Как то вроде называется, какой то тип полиморфизма.

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

И никакая "девиртуализация" невозможна с CRTP, хотя бы потому что CRTP ЭТО НЕ ВИРТУАЛИЗАЦИЯ. Что девиртуализировать, если виртуальных функций нет?

А можете теперь извиниться за свою невнимательность? Я говорил о том, чтобы полагаться на девиртуализацию вместо CRTP.

Это вообще не то как надо использовать CRTP

Это опечатка, в примере надо передавать объект в функцию по ссылке, а не по значению, конечно.

Используем CRTP = отказались от динамического полиморфизма = у компилятора есть возможность для девиртуализации

здесь чётко для не знакомого с темой связывается CRTP и возможность девиртуализации. Куда ни глянь - принимают CRTP за альтернативу виртуальным функциям. Это нужно исправлять, на уровне всей отрасли уже распространилось

Это опечатка, в примере надо передавать объект в функцию по ссылке, а не по значению, конечно.

и это никак не исправит тот факт, что CRTP совсем не для такого использования. Если уже сделали шаблон - ну зачем вам IAbc там? Это же ничего не изменит. Только запутает читателя.

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

здесь чётко для не знакомого с темой связывается CRTP

Здесь чётко обрезана цитата на удобном вам месте, дальше там "..., закатывание солнца вручную через CRTP может оказаться избыточным".

Куда ни глянь - принимают CRTP за альтернативу виртуальным функциям.

Да, в этой статье. В этом бенчмарке. Понятно, что с огромными ограничениями, потому что динамического полиморфизма нет.

Это не нужно делать публичным интерфейсом, оно не является интерфейсом

Но его используют в этом качестве.

Я не готов глубже обсуждать, потому что сам им почти не пользовался. Но мой взгляд на CRTP вот такой.

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

Мне интересно услышать мнение Хабражителей и разработчиков на C++ — кто из вас строит проекты без кучи?

Почти не использую runtime аллокацию, стараюсь по максиму переиспользовать уже выделенную память. У меня правда проект вообще без графики, картинку отрисовывает уже браузерный клиент. Зато работает непрерывно уже 3 месяца.

Используем на проекте комплексный подход.

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

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

Скрытый текст

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

плюс avx если поддерживается на устройстве

Когда я вижу такие статьи, то всегда думаю, чувак, где код, где бенчмарки ? Чтобы мы могли проверить, а не только на графики посмотреть. Иначе, это пока что "теория".

Да, какие-то профиты вы получили, не сомневаюсь, но какой ценой ? Но когда вы пишите, что в variant до 4 элементов будет switch, это же какой-то женский половой орган бро. А в общем случае что будете делать ? И сколько выигрыш.

Статья, в которой надо большими жирными буквами писать перед каждым абзацом "применять только после тщательного анализа тулзами", иначе вы учите плохому. Т.к. код усложняется х100, а профит в каждом отдельном случае еще неизвестен. Т.к. ребята в glibc и co, не тупые, если вы смотрели когда-нибудь код там (или чинили баги в этих бакетах и видели их оптимизации, там тоже есть хитрости).

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

Но, у вас это звучит прям как религия, а не оптимизация узких мест с доказательствами :)

В геймдеве это стандартный и общепринятый подход. Просто за пределами геймдева он редко где востребован, буквально еще в паре отраслей.

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

Вот и бились за миллисекнды изо всех сил. Ведь времени на обработку кадра никогда много не бывает, геймдизайнеры всегда найдут на что его потратить. Например патфайндинг поумнее сделать или физику подетальнее.

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

Не только в геймдеве.

В эмбеде тоже.

Если ты делаешь софт для допустим скоростной печати на конвейере, то у тебя может быть всего 25-30 миллисекунд на рендер и отсылку в хардвер печати, начиная от получения irq триггера печати.

Если аллокатор затупил - то все, продукт на конвейере испорчен.

А железо там как правило не сверхмощные x86 а arm и не самые мощние при этом.

Кроме того надо еще и UI какой-никакой рисовать.

Единственный вариант тут - не использовать никакие обращения к куче из подобных критических мест.

Мимо, человек который не раз искал потерянные еденицы миллисекунд.

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

В целом логично,побитовый сдвиг для быстрого доступа к памяти

У ArrayPool.Shared достаточно низкий лимит количества массивов в пуле.
Проверьте, возможно у вас одномоментно требуется сверх того и пул продолжает выделять и выбрасывать слишком много инстансов.

Если так - стоит попробовать использовать ArrayPool.Create со своими настройками. Но он - ConfigurableArrayPool заметно медленнее в многопоточном high throughput. Нужно будет отбенчмаркать.

В моём случае переход со стандарного ConfigurableArrayPool на самописную адаптацию стандартного же SharedArrayPool дал очень ощутимый прирост. Но я микро и нано секунды ловлю, на уровне миллисекунд может и не заметно разницы в скорости.

 То есть можно 512 или 1024 байт, но нельзя 768. Это очень неудобно, приходится ломать логику.

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

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

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

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

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

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

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

а кто и на чем тогда играл в эту игру в 2000-е?

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

Про сотни тысяч игроков я не приукрасил, ежедневное число игроков проекта моей команды частенько переваливает за 500к в стиме.

А у моей команды онлайн в стиме частенько переваливает за 500 миллионов. Я как тот дедушка из анекдота.

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

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

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

спасибо.

КДПВ как бы намекает про что речь

мне эта картинка ни о чем не говорит.

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

Я по картинке в статье сразу понял, — но молчал)))

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

забыл как в старкрафте(обновлённом) она тоже ртс, рефорджед не знаю подходит ли, не знаю ртс он, но нулевой или первый вполне как учебник разработки ртсок )

я сейчас детально присмотрелся к opengl что у меня происходит в коде,

  • создаю вершины 1 кубика

  • выделяю видео память под эти вершины

  • создаю вектор обьектов

struct Object3D {
  glm::vec3 position;
  glm::vec3 prevpos;
  glm::vec3 velocity;
  float spped;
  float size;     // для кубика
  float radius;   // для сферы
  bool isSphere;
  bool collided = false; // для визуализации столкновений
  bool stayObj=false;
  std::string name;
};
std::vector<Object3D> objs;
for (x3000)
отдельно в конце создаю ограничительный кубик большой
  • далее

    // Построение BVH
    std::vector<Object3D*> objPtrs;
    for (auto& o: objects) objPtrs.push_back(&o);
    BVHNode* bvhRoot = buildBVH(objPtrs);
  • далее в цикле

        // Проверка столкновений
        for (auto& o : objects) o.collided=false;
        for (auto &o : objects) {

            if(!o.stayObj) o.position+=o.velocity;

            traverseBVH(bvhRoot, &o);//солвер при обходе
	    
        }

...................

и еще раз чтобы отрисовать
        for (auto &o : objects) {
          if (o.stayObj) glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
    	  else glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
            glm::mat4 model=glm::translate(glm::mat4(1.f), o.position);
            float scaleVal = o.isSphere ? o.radius : o.size;
            model = glm::scale(model, glm::vec3(scaleVal));
            glUniformMatrix4fv(locModel,1,false,&model[0][0]);
            glUniform3f(locColor, o.collided ? 1.f:0.f, 0.f, o.collided ? 0.f : 1.f);
            glDrawArrays(GL_TRIANGLES,0,36);
        }

и вот это весит 150 виртуальной и до 50 мегабайт реальной памяти

Скрытый текст
c -O3 10%
c -O3 10%

помойму со стеком в проходе с sse avx получше без рекурсий если

может 4к текстуры и АИ и еще что-то догружает, вообще стек directX12 тоже грузный если по метро смотреть, в ютубе видел

а ну правильно, свет/нормали/тени/шейдПостпроцесс/всякиеМапыПоддерживающие, чтобы не считать и поидее память улетела

Почему бы Вам не собрать эти идеи в отдельную статью? Сейчас это выглядит как разрозненный поток мыслей - похоже за это и минусы к комментариям.

почему поток, если текстуры 4к(свет/тени(пенумбра)/нормали) то мапы на них и внутрянка плюс выбор граф. апи даёт свои результаты на память, вы не приводите более детальных примером выделений во фрейм тайме поэтому приходится учитывать весь контекст по работе с памятью

почему поток, если текстуры 4к(свет/тени/нормали) то мапы на них и внутрянка плюс выбор граф.

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

но ведь пулл обсуждается не раз вами, и вы просто обсуждаете как что в отрыве от полной реализации примера, все примеры наверно будут индивидуальные

ну есть библиотеки например просто ради теста можно заюзать glm, bvh видел есть(или bullets,box2d), может еще что-то есть

ну вот вы сами уже тезисов для статьи накидали, еще добавить примеров использования и получится неплохая техничка

да у меня поднакопилось много материала, в т.ч. и сейчас уже можно мини демку показывать, эх да, ладно прошу тогда извинения, мне оказывается не хватало всего-то BVH, я ток что дореализовал полный 1 уровень, плюс кусок столкновений), ну и плюс другой материал, да вы правы прошу прощения

Звучит история, мягко скажем, странно.

  1. Такие вещи делаются не через "я отклоняю коммиты", а через единый гайдлайн для команды.

  2. В большинстве случаев использование стандартной кучи вполне оправдано, но раз уж вы столкнулись с ситуацией, где она не подходит – то должно быть чётко расписано, что, как и почему. И проведено исследование, что именно лучше всего подходит для ваших задач. Далее пишется гайдлайн и см. пункт 1.

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

Если гайд обоснован (в вашем случае выглядит обоснованным) – его форсит тимлид.

А если вы начинаете просто навязывать свой подход коллегам – коллектив избавляется от вас, и всё возвращается на старые рельсы.

Третий случай – когда коллеги с идеей согласны. Опять же, описываете её в гайдлайне, и его форсит как тимлид, так и коллектив в целом навязывает новичкам – как обезьяны из "здесь так принято".

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

Пришел к точно такому же подходу. Но не из страха аллокаторов, тут много плюсов:

  • Понимание ограничений игры. Сколько максимум будет объектов\событий в кадре.

  • Если все игровое состояние лежит в одной большой структуре, то его легко сохранить/восстановит просто скопировав память.

  • Подход с CRTP (не знал что так называется) или через tagged union просто на практике показался более удобным чем огромные списки наследования.

Простите, а можно задать вопрос дилетанту? (Может быть, придётся, однажды, и игры разработать. Если повезёт.)

1. А какой другой вариант?

2. Можно ли сразу взять большой кусок памяти и назначать конкретным объектам определённые фрагменты?

3. Почему C++? Может быть, имеет смысл создать какой-то специальный язык программирования для разработки игр? И если C++, зачем нужны библиотеки общего назначения, вроде STL, а не специализированные библиотеки?

Я (к сожалению) очень далёк от этой интересной области, но если бы я пытался делать игры, то полагался бы также и на стек, создавал бы батареи объектов в стеке, а куча нужна для особо «текучих» случаев. В принципе, её использование можно и ограничить. Но для этого надо задавать какие-то ограничения.

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

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

соотв самый оптимал или делить пространство, или делать иерархии или миксить в универсальное, деля пространство (например рейкастинг в иерархию почти всегда нужен, потомучто надо отсечь то что не будет участвовать в коллизии, далее, статика просто стоит, все со всеми например, можно деревом, а можно конечно же и массивом, но массив 1 000 000 это больше проверок, чем поделить пространство и пробежаться по маленьким подпространствам, так что пулл, интересно, но в пулле должны быть деревья, а в дереве будет как раз пол игры)

Эффективный-алгоритм-обработки-коллизий-n-кругов

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

С многопоточностью в C++ все что нужно есть со времен C++11. Просто в соответствии с принципами плюсов стандартная библиотека не пытается ни продвигать "единственно верный" подход (ибо их много), ни предлагать кучу возможных вариантов (зачем если можно взять стороннюю библиотеку под конкретно Ваши цели).

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

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

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

без накладных расходов виртуальных функций

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

Виноват, проглядел call rax, девиртуализовались и заинлайнились только первые 2 вызова, третий нет.

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

Истину глаголите! Подписываюсь под каждый словом!

В электронике есть агрессивная сета свидетелей динамической аллокации во встраиваемых системах, распространяющая скверну. Дискуссию не вывозят, но очень упёрты.

Sony зафиксировала низкий фпс, коррапшены в памяти и деградацию производительности — ага, а мы думали они там просто в билд играют, и отказала в одобрении игры для публикации

Всякие юбисофты, так понимаю, приносят сильно больше денег, поэтому к ним не относится?

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

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

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

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

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

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

Мда, вся статья нвписанна из-за того, что не способны написать пул и алокатор для stl. В системах рантайм такое используется, возмите на заметку.

А в целом, алокация через системный вызов нигде не обещена как O(1), а значит использовать просто так запрещанно

Не копали ли вы в сторону собственных аллокаторов? Это естественно, что стандартный аллокатор не может идеально удовлетворить все потребности, он же как швейцарский нож - может всё, но всё не идеально. А можно сделать скальпель под себя. Без рассмотрения данного вопроса отказ от кучи может быть преждевременным.

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

С описанными проблемами не сталкивался.

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

Дааа, видно как хорошо оптимизированы современные игры

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

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

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

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

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

Да, майнкрафт лагает. Да, потому что на Java.

для Ява программ майнкрафт на удивление не лагает. вероятно там тоже арены итп

лагает, например, идея

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

наверняка можно, но ведь в яве есть очередь чанки там компактные, и координаты вагонетки известные, я клоню к тому что в очереди просто путь вагонетки по 3сняли 3(там на деле может и по 9 ну тоесть 256х256х256х9 игрок в центре, на выбраном максимуме, тоесть наверху) чанка добавили в очереди вперед поидее влезает

может как-то и проще ограничивают, а очереди на фоне отрабатывают

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

еще зависит от механики добавления куба, убирания куба(если механика ексклюзивная, тоесть индивидуально обновляет это один момент, а может просто простримить чанк в память и мы даже не заметим, там память быстрая(типо сразу вектор памяти - чанк unmap-map в gpu)), получается если в 100 раз быстрее кликать попадаем в гц, но я не думаю что так будет

Я слышал что аллокрейт в майнкрафте с годами может доводить до 200МБ/тик

Итератор - аллок на хипе, класс, который вполне мог бы быть обычной структурой - аллок на хипе, даже простые операции типа перемножения матриц это либо аллоки, либо кэшированные в виде полей класса инстансы

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

Это касается Oracle JVM и может быть OpenJDK, а ведь есть еще и ведро и арт, по крайней мере раньше, легко мог фризнуть процесс на 16мс пока делает сборку мусора.

Когда я писал игру под совсем ретро гаджеты, кэширование полей и выкидывание явных итераторов давало большой буст к стабильности фреймрейта именно за счет уменьшения нагрузка на GC, но да, сейчас это не так актуально. До какого то определенного момента можно забить и делать 'тяп-ляп'.

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

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

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

Увы, к итераторам это напрямую не относится, потому что итератор создаётся в методе iterator() и возвращается из него. Чтобы данная оптимизация сработала - JIT должен сначала этот самый iterator() заинлайнить, чему в свою очередь мешает виртуальность этого самого метода.

Кстати, у ваших неизменяемых классов та же проблема.

И если даже надо, то у каждого потока есть свой собственный кусочек памяти, когда он быстро и легко может насоздавать временных объектов.

А вот эта штука и правда помогает.

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

В отличие от многих приложений - игры, особенно большие, часто работают часами без прерываний и должны поддерживать стабильный...

Да, полно приложений, которые должны часами работать, если не месяцами. На играх и Блокноте с Паинтом мир не заканчивается.

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

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

Выводить в ноль аллокации слишком дорого, да и нет смысла в zero allocation rule. Всему есть своя цена, и в определенный момент время разработчика становится дороже того профита что мы получаем.

Прикольно, разработчики игр дошли до того с чем разработчики встроенных систем воют уже лет 20, с тех пор как С++ начали внедрять в embedded. Однако разница в том что встроенные системы должны работать без фризов и перезагрузок годы а не часы, как игры. Но приятно что автор дал ссылку на библиотеку Embedded Template Library (ETL), она позволяет реабилитировать С++ для разработчиков встроенных систем чуть больше чем на половину.

Haskell реабилитирует полностью ;) мы на него с C перешли

:-) То есть С++ используете но он становится прекрасным на фоне / вместе с Haskell? И прямо таки полностью перешли и С в вашем проекте не осталось? И монады почти не используете? Не боитесь что рекурсии приведут к падению из-за исчерпания свободной памяти? А сборщик мусора не дает фризов, или это не real time?

То есть С++ используете но он становится прекрасным на фоне / вместе с Haskell?

Косноязычно получилось. Нет C++ не используем, используем Haskell вместо.

И прямо таки полностью перешли и С в вашем проекте не осталось?

C остался тоже. Библиотеки вендоров, например

И монады почти не используете? 

Из-за них и взяли Haskell.

Не боитесь что рекурсии приведут к падению из-за исчерпания свободной памяти?

Не боимся. В Haskell коде они есть, а в самих прошивках нет

А сборщик мусора не дает фризов

На железе по итогу все работает в статической памяти без кучи и даже почти без стека

или это не real time?

Жесткий реалтайм, без использования ОС с кооперативной многозадачностью, до 500К IRQ в секунду

Удивительное рядом, походу компилятор превосходит самые смелые ожидания. Поделитесь названием компилятора? Это он сразу в машинный код компилирует, или через промежуточный язык типа С ?

Обычный GHC. Мы пишем код на Haskell с использованием eDSL Ivory. Получаем программу, которая генерит C99 код и компилирует его с помощью любого компилятор си компилятора (мы GCC используем).

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

На выходе получаем машинный код. Haskell служит таким мета-инструментом над C – вроде, шаблонов на стероидах.

Sign up to leave a comment.

Articles