Комментарии 46
А в игровом движке (где, казалось бы, важна производительность) повсеместное использование умных указателей оправдано? А то видел недавно код (правда, немного из другой области) с атомарными инкрементами/декрементами, растущими из изменения счётчика ссылок, в числе основных хотспотов.
Да, если без фанатизма, то все впорядке. На крайний случай в какие-то тяжелые алгоритмы можно и сырой указатель передать. Умные указатели решают проблему утечек и менеджмента памяти, что в играх весьма актуально
На крайний случай в какие-то тяжелые алгоритмы
На мой взгляд это и есть главное в движке (всяческая физика или её аппроксимация, работающая с адекватной скоростью на реальном железе), а всяческое построение деревьев объектов и т.п. - вспомогательная обвязка. В этой обвязке умные указатели, конечно, вполне имеют право на жизнь.
Если игра не слишком большая, то нормально. Если объёектов планируется спавнить много, то лучше жить с ECS, аренами и сырыми указателями. Ещё есть вариант похожий на gc - autorelease pool, как в кокосе. Там можно ручками инкрементить счётчик ссылок. Проблемы там правда похожие получаются и следить за объектами несколько сложнее, плюс дефолтная реализация управление пулом без допиливание напильником начинает заметно фрагментировать память в некоторых ситуациях. Если есть какие-то ограничения по памяти, то это надо будет патчить.
Нет ли здесь:
// 4. Создаем inplace объект за счетчиком
_type* object = new (memory + counterSize) _type(std::forward<_args>(args)...);
проблем с выравниванием для типа _type
?
Скажем, sizeof(counterSize) у вас 4, а выравнивание для _type
-- 8.
В целом да, здесь явно не учтено выравнивание, но блок счетчика подбирается как раз под размер выравнивания, поэтому проблемы нет
Кем и как этот размер подбирается?
кхм.. мной, с учетом платформ. Ничего специфичного пока в моих планах нет, скорее всего размера 4 + 4 будет достаточно для всех платформ - ios/andoird/mac/win/linux/web. Ну а если понадобится, конечно, добавлю выравнивание здесь
Выскажу неожиданную точку зрения - вся эта возня с умными указателями указывает на плохой дизайн проекта, где не было изначально продумано, кто какой объект создает и уничтожает. При правильном проектировании сырых указателей вполне достаточно.
При правильном проектировании сырых указателей вполне достаточно.
Это утверждение относится к игровым движкам или к программированию на C++ вообще?
Вообще.
Ясно-понятно, вопросов больше не имею.
Ну если всё ясно-понятно, то объясните, как создавали программы на C++ до появления умных указателей.
Во-первых, сложно и геморройно. Охота за утечками памяти в начале-середине 1990-х годов была одним из основных занятий при разработке на C++.
Во-вторых, умные указатели появились отнюдь не вместе с C++11. Я сам их начал использовать на повседневной основе во второй половине 1990-х (при этом не был пионером, а подсмотрел в чужих разработках, так что умные указатели в мире C++ уже лет тридцать как, если не больше). Ну а самый простой из них под видом std::auto_ptr был прямо в С++98.
Охота за утечками памяти происходит именно из-за кривого дизайна, когда нарушена парность new/delete.
Так очевидно же, что писать на C++ без утечек памяти, use after free и double free просто: достаточно не совершать ошибок. Делов-то.
Именно про это я и говорю: умные указатели - это заметание ошибок дизайна под ковер.
Охота за утечками памяти происходит именно из-за кривого дизайна, когда нарушена парность new/delete.
Да ну? Мне вот кажется, что кривой дизайн это как раз тот, что подстроен под особенности языка.
Да и возможно ли вообще писать на С++ так, чтобы программист не мог забыть про освобождение памяти? Я очень сомневаюсь.
Goto использовали
объясните, как создавали программы на C++ до появления умных указателей
Так же, как делали вообще всё в IT: долго, геморно, с кучей ручной работы, которой заниматься трудно и скучно.
Хм, и я так считал. Но у меня буквально был опыт сравнения двух подходов - сырыу указатели в домашнем проекте и умные на работе.
Тем более что движко позволяет писать игровую логику на С++, и там вероятность ошибки слишком велика. Поэтому умные указатели здесь решают
И да, и нет.
Игровой движок, чаще всего, так или иначе позволяет писать игровую логику, манипулируя абстракциями этого самого движка. Отсюда вытекает, что помимо внутреннего устройства движка нужно помнить о том, что пользователь этого движка может написать не самый качественный и продуманный код (а в геймдеве это обыденность) и, как итог, замучается искать ошибку.
Если же движок наружу отдает не сырые указатели, а умные, это будет форсировать пользователя движка так или иначе пользоваться умными указателями и, потенциально, это снизит количество проблем, связаанных с менеджментом памяти.
В целом, чаще всего на хорошее проектирования нет времени - фичи нужны здесь и сейчас.
Плюсую. Как бы аккуратно не был сделан менеджмент памяти в движке, в игровом коде будет ад и содомия
А вы сделайте свою библиотеку так, чтобы пользователь не мог ничего плохого сделать по сырому указателю..
Ваш довод имеет право на жизнь и я с ним, скорее, согласен, чем нет. Но нельзя исключать человеческий фактор, который накладывается на сверхсжатые сроки реализации геймплея. Внизу фрагмент кода, который я наблюдал не на одном игровом проекте.
Entity* gameObject = Engine::createGameEntity(/* some args */);
// some gameplay logic here
..............................
// some code inside multiple if statemets
{
delete gameObject;
}
..............................
// and here we go again
if(gameObject)
gameObject->SomeFunc();
Проблема ли это движка? Нет. Но если бы движок форсировал использование умных указателей для таких сущностей, то большинства ошибок от банальной невнимательности можно было бы избежать.
Это не оправдание, конечно, суровых реалий геймдева, но иногда приходится искать баланс между "правильно" и надо еще вчера.
Я по работе пишу на Unity и C#, с низкоуровневой разработкой игр и движков знаком, но не сильно, только учусь (язык Odin). Но смотря разные видео-лекции, от того же Кейси Муратори, Майка Актона (знаменитый доклад по DOD), или Джонатана Блоу. Все они топят против умных указателей и против массивов указателей в целом.
Мол делай плотно упакованные массивы данных, вместо указателей используй индексы, смотри не на отдельные объекты, а на множества объектов, используй кастомные аллокаторы вместо new, аллоцируй и переиспользуй большие блоки памяти, и твой код будет работать в 5-10 раз быстрее, при тех же усилиях, а умные указатели отпадут за ненадобностью.
Что думаете про подобный подход? Писать как говорят джентельмены выше на практике слишком сложно? Или если оверхед небольшой, то и ладно?
Это и правда классный подход, но имхо не для всего подходит. Все таки игра не это не только (и далеко не всегда) туча бегающих юнитов, куча рассчетов и т.п. Есть куча UI, особенно в мобайле, где все это просто вредит, т.к. код становится сильно сложнее.
Для такой не требовательной к производительности логике нужно максимальное удобство, и в С++ это дают умные указатели и более простая схема работы
С другой стороны, в AAA игре, выжимающей максимум из возможностей CPU/GPU на физику, графику и т.д., где надо следить, как объекты в памяти размещены относительно кеш линий и страниц, логика и структура игровых объектов тоже заметно сложнее, чем в казуалке - и как то умудряются совмещать оптимальную раскладку объектов в памяти и манипулирование объектами без постоянных ошибок.
Такой подход используется в том числе и в казуальных играх, правда, в чуть меньших масштабах.
Основная проблема - сложнее поддерживать и изменять такой код. В геймдеве в течение недели могут приходить правки по игровой механике, которые сравнимы с тем, чтобы переписать все заново. Поэтому в геймдеве часто встречается низкокачественный код, что ведет к очевидным ошибкам даже на банальном манипулировании объектами в куче.
То, что Вы описали, чаще всего, применяется в каких-то частных случаях. Например, при оптимизации какой-либо подсистемы, которая уже точно будет существовать в каком-то плюс-минус определенном виде.
edit: к слову, насчет умных указателей и иже с ними: в Unreal Engine они активно используются, а это игровой движок, который по праву можно назвать флагманом среди аналогичных разработок.
Насчет поддержки не знаю, возможно соглашусь. Автор цитаты которых я приводил, не согласились бы, Майк Актон говорил, что логика становится заметно проще, 0 абстракций, прозрачный алгоритм, но это ладно, я не настолько опытен, чтобы судить.
Касательно Unreal, умные указатели там есть, но из того, что я видел, работая с ним, часто используются сырые, потому что в движке используется сборщик мусора. В Unigine в плюсовой апишке тоже сырые, и погуглив тоже нашёл информацию про сборщик мусора. В новом O3DE всякие Entity/Component тоже по сырым (вроде без GC). Умные указатели видел только в недавнем UltraEngine. Так что все флагманы стараются от умных указателей уйти, пусть даже в сборку мусора.
Со сборкой мусора возникает резонный вопрос - зачем и по каким причинам ему отдается предпочтение, а не более, на первый взгляд, маловесной системе из умных указателей. Я не видел статей, которые бы сравнивали эти два варианта менеджмента памяти на каких-то реальных или около реальных кейсах.
Насчет нуля абстракций и простоты логики - это все зависит сильно от проекта, на самом деле. Где-то да, это даст максимальную простоту, а где-то это в итоге приведет к ненужным усложнениям во имя "оптимизации". К тому же важно помнить, что игровые студии предпочитают экономить не ресурсы компьютера, а время сотрудников. Так что вопрос такой, не самый простой и от случая к случаю можно получить очень разные мнения и результаты.
Я работал с чем-то отдаленно похожим на ECS. В плане производительности нареканий не было совершенно, но иногда уходило прилично времени на то, чтобы найти нужный компонент и нужную систему для того, чтобы внести в них правки. Но т.к. проект требовал множества сущностей одномоментно, это был разумный компромисс.
В целом, так уж получается, что подходы подстраивают под нужды и цели бизнеса, а не наоборот. Отсюда, в общем-то, появляются всякие абстракции, библиотеки (и движки тоже) и все движется к тому, чтобы можно было взять любого программиста на стеке Х и безшовно заменить его на другого из этого же стека. Мое личное мнение, что именно на этом все завязано. Именно отсюда идут корни всяких книг навроде "Чистый код" Роберта Мартина, именно поэтому мы начинаем жертвовать ресурсы машины во имя бизнес-процессов.
Если коротко суммировать то, что я написал - все зависит от конкретного проекта, его специфики, масштабов, конкретной студии, которая его разрабатывает и итоговых целей этого проекта. Где-то можно пожертвовать половину бюджета кадра ради того, чтобы студии было проще жить, где-то предпочтение нужно отдать производительности проекта. Отсюда выбирается уже методология и подходы работы.
shared_pointer - это понятно, а можно наоборот? Есть несколько указателей на объект. Если я по одному из указателей сделал delete, то всем остальным бы привоился nullptr?
weak_ptr
ага, метод expired(), спасибо. А что если объект был уничтожен, а потом была выделена новая память и так случайно получилось, она выделилась в том же месте, где был старый объект, expired по-прежнему вернёт true?
Насколько я понимаю, weak_ptr разделяет управляющую структуру с shared_ptr и пока существует хотя бы одна ссылка (weak_ptr или shared_ptr), то управляющая структура не будет освобождена и соответственно ее память не может быть повторно использована, а в этой структуре указано был ли освобожден shared_ptr, что будет влиять на результат expired и lock.
Рефакторинг игрового движка: от сырых указателей к умным