Комментарии 14
Должен сказать, что на фоне статей с проверками на баги, эта статья не продаёт.
Слишком много ментальных усилий ради кода, о котором думать вообще не хочется, этож бойлерплейт, про который работает -- не трожь и не думай...
Вот за такое, руки об батарею отбивать и надро. Контейнеры не просто так в геймдеве не любят использовать, предпочитая рисковать ручным управлением памятью. Накладные расходы просто гигантские. А надёжность, при норм программисте - минимальна.
Грубо говоря, вы в своём примере "облегчили" код на 10%, устранили баг с шансом на миллион, и после утяжелили его в 30 раз на ровном месте. Офигеть оптимизация, никакого проца не хватит.
Геймдев это не ваше. Примеры пишите на офисных прожках. Ну, не на СУБД же, да? xD
Контейнеры сами по себе не тяжелее сырого указателя. При аккуратном применении, стоимость умного указателя в рантайме ноль.
Равно как и у контейнеров.
Стоимость их оплачена во время компиляции
Было бы чудесно, если бы так (приходится ведь списки делать, которые ну нифига не безопасные, и не всегда можешь оптимизировать и так далее). Но нет. Как показывает практика, а она, как известно, критерий истины, работа с контейнерами с большим количеством постоянно "перемешиваемых" данных, знатно уступает "самопальным" вариантам по скорости. Вплоть до двух порядков. Контейнеры, они, ведь разные бывают, ага. И когда начинаешь работать с массивами оных, особенно динамических, то что ты платишь за безопасность и простоту - просто замечательно видно. На глаз, не какие профайлеры не нужны.
Такова грустная селяви использования контейнеров и иже в геймдеве. Во всех остальных направлениях, это чудесный и удобный вариант. Для геймдева, увы, нет. Впрочем, можно наплевать на ЦА (как впрочем, большинство и делает), и перекинуть заботу об этом - на конечного пользователя. Если де игра не запускается, значит, компостер слабоват, и иди его грейди. Де, твои проблемы. Собсно, нынешние проблемы с процессорами вызваны как раз такой практикой, и таким отношением прогеров к ЦА. То, что они сужают круг своих же собственных покупателей, к слову, доходит далеко не до всех. Что очень забавно было бы, если бы не печально (геймдевелопер должен учитывать такие вещи, а не игнорировать, абы было удобнее). Потому как говорит о том, что "набирают по объявлению". И игровой индустрии еще не один год приходить в себя после завала 08 года.
А можно не "на глаз" а с примером? Повторюсь: оплата идёт в момент компиляции при аккуратном применении. Тот же unique_ptr буквально компилируется в один указатель, никакой разницы с сырым указателем нет от слова совсем. std::array<X> ничем не отличается от X[], кроме дополнительной безопасности и контроля во время компиляции. Какие именно контейнеры уступают чему именно, можно примеры? А то попахивает что перемножаем яблоки на апельсины и жалуемся что на выходе лампальсины.
А если говорить про "чудесный геймдев"... Я вот вижу насколько блюпринты медленнее си -- и ведь не сказать что все бросили все блюпы нафиг и обходят их за километр, да?
Да блин, посмотрите любые потроха контейнерных классов. Или там просто так, пару сотен тысяч строк кода ?
Эта "безопасность", проверяется там всегда, при каждом добавлении, удалении и так далее. Плюс ресайз, плюс дофига чего. Это всё динамически. Как это вы оплатите на момент компиляции? Если сами не знаете, к примеру, будет обращение к массиву контейнеров или не будет такого вовсе? Как можно "оплатить" при компиляции? Нет, это всё оплачивается во время работы.
Вы вообще знаете, что к примеру, для игр, используют не просто "сырые" указатели, а вообще, самопальные версии "new/malloc" (ну, понятное дело, с нужной обвязкой)? Так как стандартный - слишком медленно, а упарываться выделением памяти через ассемблер - тут выгода слишком мала. Поэтому выделяется сразу здоровенный кусок памяти, куда уже свой собственный менеджер памяти распихивает ресурсы как ему будет надо и как удобно. Не знали про это? Ну вот. Поэтому использование контейнеров, которые надёжно, дубово и при каждом обращении проверяют "всё ли на месте? Всё ли правильно" при просчете сцены с несколькими миллионами вершин - сделают вас седым раньше срока. Если знаете хозяина gamedev.ru Сержа "wat" то может вспомните, как он пытался воткнуть сцену на тех еще компах (ну как 900 мгц Атлон, зверь на то время был) через контейнеры. Де, это мол, стильно, модно, молодёжно. В итоге сцена которая грузится менее секунды, грузилась более пяти минут. Ну, на отрисовку почти, не влияла, это да. Если только незначительно крайне, порядка сотых, тысячных дельты. Ну это и очевидно, она целиком уже в видеопамяти была в нужном виде. После издевательских шуток на форуме, с контейнерами он для графики, завязал. Как сейчас, не знаю )
Если очень интересно, почешите исходники (они есть) контейнеров. Поучительно. Как и внезапное осознание того, что это шаблоны классов. В которых дофига различных и очень непростых функций, даже в самых примитивных казалось бы, контейнерах, не говоря уже про более сложные виды.
Да блин, посмотрите любые потроха контейнерных классов. Или там просто так, пару сотен тысяч строк кода ?
Я их видел много кратно. Нет, не просто так. Однако активное использование шаблонов и constexpr -- позволяют оплатить их во время компиляции. unique_ptr делает кучу проверок... Которых не остаётся в скомпилированом коде. Вот простейший синтетический пример: https://godbolt.org/z/ePE68Pob3
Вы вообще знаете, что к примеру, для игр, используют не просто "сырые" указатели, а вообще, самопальные версии "new/malloc" (ну, понятное дело, с нужной обвязкой)?
Вы вообще знаете, что контейнеры прекрасно работают с аллокаторами? https://en.cppreference.com/w/cpp/container/vector/vector
"Свой аллокатор" это очень полезная штука, не только в геймдеве. Причем код, работающий на контейнерах, прекрасно можно переиспользовать в одном приложении сразу используя с разными аллокаторами без ручного управления.
Поучительно.
Да, вам стоит почитать исходники и посмотреть на результат компиляции. Это действительно поучительно.
Действительно есть места, где стандартные контейнеры не подходят, но ответ почему не подходят должен быть осмысленен, оценен и подтвержден -- а не просто проекция страхов на уровне "кто-то там когда-то во времена атлона напел".
Если "округлить", то могу по поводу этого "заранее оплачено" ответить так, что в случае, когда можно заранее предсказать количество юнитов которые будут храниться в контейнерах, это действительно почти не даёт накладных расходов (почти, но там реально уже мизер, смысла нет из за этого бодаться, код действительно более надёжный, за такую, небольшую цену). Но есть нюанс. Вы должны полностью и целиком знать сколько и чего у вас будет (либо, платить овером по памяти, что к слову, может оказаться и хужее) в сцене. Если сцена динамична, то тут всё, приплыли. А ведь, большая часть современных игр, увы, не статические сцены с заранее просчитанными количеством объектов, вершин и тому подобное. На лету ситуация может меняться от 200 тыс до 300 миллионов вершин в моменте. Всё держать в памяти - ну такое себе, выделять ондеманд ? Так в глухой лаг уйдет проц. Причём ощутимый. Впрочем, при таких скачках, буду откровенен, лагать будет в любом случае, хоть всё на чистом ассемблере напиши. Тут мало что поможет в плане памяти. Так то, для графики проблем нету, что не надо - выкинет из обсчёта и отрисовки, а вот память, только заранее если с хитромудрыми алгоритмами "на опережение". Что требует совсем иных решений, не трогая уже контейнера.
И контейнеры позволяют ручную реаллокацию. В отличие от ручного realloc() -- безопасно и аккуартно.
Есть контейнеры, которые уже реализуют варианты кусочно-линейных представлений, есть контейнеры, предоставляющие цельные куски по требованию, есть списки и деревья разных цветов и пород.
Вы правда утверждаете, что ручное выделение, которое написали вы сами, будет работать лучше ручного выделения, которое написал кто-то другой?
Или ваша ручная реализация спокойно поменяет 200 тысяч на 300 миллионов вершин магическим образом, не уходя в никакой лаг?.. Сами же говорите, что нет. Так к чему этот пример был?
По поводу качества, ну, вы сами подумайте, кто и с каких "примеров" писали код. То есть, конечно, массивное выделение массивов (прасити) это долгая и нудная работа, которую мы, геймдевы подобрали, отряхнули, чутка приспособили под себя - и "шоу начинается". Но это уже отработанные, отполированные куски, или это было бы изрядной авантюрой, вы должны это сами понимать.
Конечно, убрать лишние проверки, которые ВОЗМОЖНО сработают один раз на миллион запусков программы, это обязательно, и только это даёт порядка 5% прироста производительности. Только вот убирание ассертов и прочего. На ровном месте, без алгоритмов и так далее. Так что да. Не только утверждаю, но и уверен в этом. Сам видел.
Нет, я пояснил, что в таких (!!!) масштабах, разницы почти нету, лаги будут и там и там, разница только в длительности. Но не это важно. Пример к тому, что если мы уменьшим проблемы с экстремальных до нормальных, то получим то, что вдруг внезапно, контейнера начинают подтупливать. С чего бы это. Вроде бы чуть, но, всё равно странно. Причем на простейших операциях (типа добавления).
К тому и был пример, что некоторые вещи ты должен контролировать сам, ибо кто контролирует Дюну, контролирует... Стоп, не отсюда, ибо все кто контролирует выделение памяти, контролирует и быстродействие программы (особенно, сложной). Впрочем, буду откровенен, за деньги, я бы написал бы код для этой программы, используя контейнера. Это быстрее и надежнее. А так как мне платят за результат, а не за как оно работает... Ну, очевидно что выйдет.
В целом я согласен, что специализированные самописные классы могут быть эффективнее их стандартных аналогов. Однако, не надо быть прям категоричным в этом. Есть смысл оптимизировать узкие места по необходимости, а не сразу писать всё своё :). Во-первых, своё может получиться куда менее надёжным. Во-вторых, эффективность многих встроенных классов значительно выросла, и они куда эффективнее, чем 10-20 лет назад. См. личную историю на тему std::string здесь (Вредный совет N50. Не верь в эффективность std::string).
О, стринг, да. Вот, к слову, довольно хорошо подмечено. Проблема вообще Сишных строк (н-т) это то, что там устроить вылет за границы массива, это вот как два пальца. Или постоянно считать длинну. Что к слову, не мешает иногда сделать опять же вылет за границу, просто надо гораздо больше изобретательности в создании ошибки проявить )
В целом, это годный пример.
Но есть и контр-пример.Давным давно, писал свой создатель словаря. (файл разбивается на идентичные участки, которые потом кодируются. Простенький и эффективный архиватор для огромного количества текста (уточню, писал я на 486ом, на ТС2.0, так что должны понимать, что хранить пару десятков тысяч книг было далеко не самым простым занятием в те веселые времена). Так вот, даже сейчас используя совсем другие инструменты, с совершенно другим же быстродействием, и прочее, и прочее, я бы всё равно вернулся бы к варианту с нуль-терминированными строками. Потому как одно дело, когда у тебя пропадает 2-10% в "ворде" скажем так, и абсолютно другое дело, когда у тебя пропадают эти же проценты, в тот момент когда система лопатит гигабайты данных. Это крупно две разные вещи.
Ну и конечно же... Самое главное. Оптимизировать только то, что надо оптимизировать. (Иначе нету смысла в любых ЯВУ. Только асм, только хардкор.) Зачем оптимизировать скорость работы текстового редактора, который по умолчанию будет работать быстрее пользователя, который, даже самый шустрый, будет "тупить"? :) Поэтому, при всей моей не великой любви к Шарпу, считаю, что для определённого круга задач, это прекрасный язык. За пару часов накидать какой нибудь лончер для обновлений игры? Что может быть проще. А проверните эту же хохму через WinAPI, ну вы понимаете, да? )))
Проверка игрового движка qdEngine, часть вторая: упрощение C++ кода