Первые шаги в оптимизации и полировке игры на Unity3d

    imageПосле того как я закончил мой первый проект, пришла мысль о портировании его на мобильные устройства или хотя бы запуске на встроенном GPU. Во всех гайдах по оптимизации, в одном из первых советов вам сообщат, что не стоит переживать о производительности заранее, начинайте оптимизировать, после того как все закончите, и вы постепенно приведете все в порядок. Так и я, выпустив изначально игру на десктоп, решил, что никогда не будет поздно оптимизировать ее под мобильные устройства. К сожалению, мне не удалось в полной мере достичь поставленной цели, потому как, похоже, что мобильные игры следует с самого начала разрабатывать с прицелом на слабое железо. На данный момент, для дальнейшей оптимизации под мобильные платформы я вижу только необходимость серьезно переделать геймплей и дизайн игрового мира. Однако и в текущем варианте получен ценный опыт оптимизации под Unity3d и результирующий прирост производительности более чем в 300% на интегрированном GPU.

    Давайте начнем с CPU


    Достаточно очевидный список сформировавшийся во время оптимизации:

    1. Не используйте Свойства! Поля и методы — ваши лучшие друзья.

    2. Кешируйте все, что вы получаете через GetComponent<>, сюда же входят transforms, rigidbodies и др.

    3. Старайтесь никогда не обращаться к объектам дважды для получения одних и тех же данных. Почти всегда это доступ через Свойства, от которых необходимо отказываться. Зачастую можно увидеть как в разных скриптах запрашивается позиция одного и того же объекта, что лучше заменить на кеширование той самой позиции, обновляя ее один раз внутри Update или даже FixedUpdate в этом объекте.

    4. Кешируйте всю математику. Каждый вызов Vector.Up будет под капотом вызывать конструктор, что не очень быстро. Я создал статический CachedMath класс, в который были сложены все направления, часто используемые векторы и кватернионы.

    5. Попробуйте обходиться без использования типа String. Каждая строка требует выделения памяти, и при бесконтрольном использовании строк, вы увидите, как GC остановит все потоки для своего вызова. В моем случае основными источниками строк были индикатор FPS и таймер во время гонки. Решением стало создать пул строковых литералов для всех цифр от 1 до 100. Это полностью исключило выделение строк в каждом кадре.

    6. Никогда не используйте foreach, просто замените на for, если хотите сберечь GC и драгоценное время CPU. К тем же последствиям зачастую приводит и использование шаблонных методов(generics).

    7. LINQ является еще одним источником нагрузки на GC. Старайтесь упрощать ваши LINQ выражения, или еще лучше, полностью заменять их на простые конструкции.

    8. Все строки используемые в Animator-объектах следует сконвертировать в целочисленные идентификаторы через Animator.StringToHash()

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

    10. Удаляйте все пустые методы Update и FixedUpdate. Также, если ваш скрипт использует оба или только фиксированный, то стоит подумать о переносе любой возможной логики из фиксированного в обычный Update.

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

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

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

    Теперь GPU


    С CPU советы были достаточно универсальны и они применимы в любом проекте. Чего не скажешь о GPU-оптимизациях, которые зачастую сильно зависят от конкретной сцены. Однако, если вы не используете сильной магии в своих шейдерах, то явный индикатор — это количество проходов GPU(pass-calls).

    Моя игра содержит открытый мир с океаном как основой для передвижений и несколькими островами в качестве декораций. В моем случае проходов было больше 2000, и мне удалось снизить это значение до примерно 300.

    Материалы. Уменьшайте количество используемых материалов насколько это возможно. Каждая смена материала это новый проход, также как и каждый текстурный слой внутри материала это тоже новый проход. Конечно, я несколько упрощаю и проходы формируются не так просто, но факт остается — слишком много проходов будут непомерно нагружать слабый GPU. Для мобильных устройств рекомендуют что-то в районе 40-60 проходов. Более продвинутые устройства могут обрабатывать и в районе сотни. Так что вам есть куда стремиться!

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

    Импостеры. Я предпочел заменить мои объекты с помощью импостеров (в целом это очень похоже на биллборды, но это множество текстур полученных пререндером объекта со всех сторон). Во встроенном Asset-Store от Unity3d множество готовых платных решений для LOD и импостеров. Однако я решил воспроизвести базовый алгоритм самостоятельно. Я создал скрипт-расширение редактора, который создавал копию необходимого объекта, менял его слой, затем создавал камеру которая была ограничена только этим специальным слоем, и производил отрисовку объекта в текстуры со всех сторон. Были добавлены основные параметры, как название результирующей папки с текстурами, разрешение получаемых текстур, расстояние до объекта, смещение по высоте, количество сторон и флаг для сохранения или отключения освещения во время создания импостера. После того как все действия завершены, скрипт удалял уже ненужную копию объекта.

    Спрайты. Теперь почти все объекты заменяются спрайтами на определенном удалении от камеры. Но количество проходов было все еще огромным. Тогда я обнаружил, что спрайты это далеко не всегда легковесная форма для отображения. Каждый спрайт по умолчанию триангулирует картинку, создавая множество вершин. На каждые 900 или около того(по официальной документации) вершин, создается очередной проход (официально группировка|пакетирование|batching — сохранение данных множества объектов в одну инструкцию для GPU — вообще неприменим к SpriteRenderer объектам). В то же время нельзя заменить все спрайты на полные квадратные регионы с прозрачностью, т. к. все прозрачные пиксели все еще требуют отрисовки, и GPU их не пропускает. Также прозрачность ведет к проблемам во время отрисовки всех спрайтов из-за проверки на глубину отрисовки. GPU все еще будет создавать дополнительный проход для одного или двух спрайтов, между отрисовкой множества уже сгруппированных только потому, что этого требует проверка по глубине. Единственное, что удалось сделать — это изменить тип спрайта на Multiple, что меняет внутренний механизм триангуляции, который создает намного меньше вершин.

    Упаковщик спрайтов(SpritePacker). Это последнее о чем вы должны помнить при работе со спрайтами. Чтобы явно указать для спрайта необходимость упаковки в карту атлас, нужно указать его Tag. В момент отрисовки спрайтов из одного атласа GPU не создает дополнительных проходов, даже если порядок отрисовки по глубине не оптимален для неупакованных спрайтов. Размер результирующего атласа также важен. По умолчанию он ограничен значением в 2048х2048. Это максимальный размер атласа, и он динамически подстраивается под оптимальный, в зависимости от заполнения. В моем случае этого было недостаточно для упаковки всех необходимых мне спрайтов на одной странице. Замена алгоритма упаковки на собственный, который основан на базовом, но с измененным значением размера на 4096x2048 значительно улучшило производительность.

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

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

    После всех изменений, производительность улучшилась настолько, что отключение всех объектов импостеров практически не влияет на результирующий FPS.

    Вода. В моем случае, мне необходимо было получить более производительную воду. Изначально в сцене использовалась waterProDaytime с включенным преломлением, которая подверглась минимальным изменениям для поддержки пены вдоль береговой линии. Была убрана камера преломлений и заменена на вызов grabpass. Все дело в том, что для корректного отображения преломлений, камере пришлось отключать матрицу отсечения, т. к. в противном случае — все объекты, выше уровня воды, просто не отбрасывали теней. Из-за этого ограничения, камера дополнительно отрисовывала сцену целиком, и вызов grabpass оказался в этом случае быстрее. Также был изменен параметр LOD множителя на время отрисовки отражений. Таким образом импостеры чуть дольше отображаются в воде, что дополнительно снижает нагрузку.

    Все изменения повысили производительность на интегрированном GPU с 6-8 до 22-24 кадров в секунду. По-прежнему низкий показатель, но лучшего добиться пока не удалось. Все еще рекомендую запуск своей игры на дискретной графике.

    Полировка


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

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

    В нем отсутствовали звуки, не было движения, жизни. Запустив один из проектов, я начал замечать детали главного меню, которые раньше для меня были просто невидимы. Так что я добавил все недостающее в первом приближении. Теперь выбранная кнопка анимирована, перевод фокуса и нажатие кнопок озвучены из бесплатных ресурсов в Asset-Store.

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

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

    Что было оставлено позади


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

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

    Другая цель, появившаяся во время оптимизаций в виде быстрого запуска приложения, или даже плавного запуска, также не была достигнута. Холодный старт в моем случае длится больше минуты. Причем каждый последующий запуск сокращает это время почти вдвое. Так что, похоже, это какое-то внутреннее необходимое требование Unity-плеера. Самым главным же недостатком является зависание UI потока при активации сцены. Я уже использую асинхронный вариант загрузки сцены, даже переключил его в Additive режим, однако UI просто останавливается после 90% загрузки, когда необходимо переключить флаг allowSceneActivation. Было бы здорово, если кто-то подскажет обходной путь для Unity5.x, или нечто вроде вызова события которое может потокобезопасно и с главным приоритетом изменять Ui-объекты с их перерисовкой, чтобы была хоть какая-то индикация процесса приложения.

    P.S.


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

    P.P.S
    Я описал главное, не вдаваясь глубоко в технические детали. Проект подвергся очень большому количеству изменений, и не все было описано — сейчас могу точно вспомнить, что с прошлой публикации просили разобраться с водой просачивающейся сквозь лодку — и это точно было закрыто. Также были добавлены звуки при прохождении чекпоинтов и получения медалей, как и небольшая анимация таймера на чекпоинтах и цветовое информирование о времени.
    Если кому-то интересно посмотреть все в действии, к сожалению, видео еще нет, однако триал теперь бесконечен.
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 51

      0
      Спасибо за статью, в целом познавательно, но есть одно но:
      Однако и в текущем варианте получен ценный опыт оптимизации под Unity3d и результирующий прирост производительности более чем в 300% на интегрированном GPU.

      Скажите, неужели сложно было вставить в статью, пару скринов из профайлера мол «вот до вот после», и парочку вырезок из кода, где показывались бы самые значимые улучшения с указанием на FPS/память и словами «так ок, так не ок»?
        0
        Я думал про это, но, к сожалению, не с чего снять скриншот. Исходники уже не вернуть, да и что это покажет — просто как доказательство того, что время в профайлере уменьшилось? В вашем конкретном случае все может быть иначе в профайлере — я иногда наблюдаю, что у разработчиков бывает и CPU время доходит до тысячи FPS, хотя я только недавно добрался до стабильных 30-60 с проявляющимися иногда 120 на графике. Мне все-таки кажется, что абстрактные и применимые к конкретному проекту графики из профайлера слабо кого-либо воодушевят.
        +2
        Даже не знаю каких советов больше, полезных о которых уже 100 раз писали или новых вредных. Заминусовал бы статью, да кармы недостаточно (наверное не зря).
        У вас в многих тезисах нет объяснения, почему именно так нужно делать. Попробуйте написать о причинах и найдете ошибки в своих тезисах. Для затравки:
        — Почему свойства на столько не производительны что стоит от них отказаться?
        — При использовании типа строка в фпс у вас течет память, дело в алокации памяти типом строка, не оптимальным использованием компонента Text или постоянный ребилд кеша шрифтов? Каким образом пул строковых литералов помогает не использовать строки?
          –1
          Про строки — это называется интернирование
          https://habrahabr.ru/post/172627/
          https://habrahabr.ru/post/224281/
            +1
            У меня есть один ответ — Mono. Свойства, внезапно, используются не очень оптимально, и нет инлайна простых свойств при использовании JIT, что в итоге значительно просаживает производительность. Без Mono, большая часть из того, что я написал, никого не интересует и не является проблемой.
            Я сам всегда поражался на всех туториалах от Unity, всегда используют поля вместо свойств, и я был против этого, т. к. чистый код на C# подразумевает эти самые свойства! Лишь перенеся часть свойств в поля и увидев значительный прирост производительности, я категорично указал, что в Unity не место свойствам — хотя они у меня остались в конфиге и сохранениях, где к ним идет очень редкое обращение.
            Также работа со строками — в моем случае я лишь избавился от постоянной аллокации текста, снизив нагрузку на GC(опять же кастомный под Unity), сгенерировав все строки изначально — да я держу их в памяти, но не аллоцирую каждый кадр. Забыл, кстати, среди CPU оптимизаций упомянуть лямбда выражения, каждый вызов которых также выливается в дополнительные аллокации в памяти. Меня спасло использование прямых делегатов.
            Unity постоянно стремится к все новым оптимизациям, но пока — все что было описано, критично било по производительности — именно это моя основная причина. Ни один из описанных пунктов бы не появился здесь, если бы я лично не увидел положительного эффекта от каждого из них.
              0
              Может тогда помог бы IL2CPP?
                0
                Забыл, кстати, среди CPU оптимизаций упомянуть лямбда выражения, каждый вызов которых также выливается в дополнительные аллокации в памяти.

                Не совсем так. Если потрогать свойства любого инстанса из такого колбека / подписчика, то да, для вызова подобного замыкания потребуется сохранить ссылку на инстанс класса. Делегаты, собственно, так же работают — так же течет память при подписке. Если не трогать свойства инстанса / сделать вызов статичного метода — память на сохранение ссылки на инстанс не выделяется. Решение этой проблемы не сильно приятное — сохранять ссылку не на метод, а на инстанс. Самое простой способ — это сделать интерфейс с методом и реализовывать его во всех классах, методы которых нужно дергать. Тогда в подписку поедет не метод, а инстанс с гарантированным методом из интерфейса, который уже нужно будет вызывать.
                +1
                Почему свойства на столько не производительны что стоит от них отказаться?

                Потому что это синтаксический сахар вокруг методов «get_ИмяСвойства» / «set_ИмяСвойства» — на количестве итераций больше пары сотен можно получить прирост производительности больше чем в 10 раз. То же самое касается перегрузок операций с векторами: +, -, * и тп. Все это внутри реализуется через вызов методв, а это накладные расходы. Простой пример:
                float Mathf::Sin(float v) {
                    return (float)System.Math.Sin((double)v);
                }
                

                Вот просто развернув этот метод в прямой вызов "(float)System.Math.Sin(v)" в своем коде получим 2х ускорение — на большом количестве итераций это дает очень хороший прирост. Вся проблема в том, что нет поддержки inline-включения методов в той версии моно, которая используется в юнити. Рекомендация (не обязательство) по инлайну тела метода появилась только с FW4.5.

                дело в алокации памяти типом строка

                Покажите кастомный алокатор для штатного типа string, будьте любезны. Проблема в том, что строки иммутабельны и обходить это можно только работая с ними как с костылями поверх массивов char-ов, но на выходе большинство апи-методов требуют штатный типа «string».
                +1
                Маловато картинок. Хотя бы скриншот «мёртвого» UI остался?
                  0
                  По CPU возникло несколько вопросов.

                  1. В каком смысле не использовать свойства? Какие свойства? Если имеется ввиду в своих классах, то допустим нам нужна инкапсуляция. Вопрос, в чём отличие свойства от геттера и сеттера реализованных методами?

                  2. Про GetComponent<> вообще отдельный разговор, я не совсем понимаю зачем его использовать в принципе. Это конечно, наверное, ускоряет работу (хотя я считаю, что это банальная лень программистов), но в большинстве случаев можно обойтись вообще без него. Т.е. компоненты, которые висят на объекте всегда, можно просто сериализовать. А те объекты, которые цепляются динамически откуда-то можно хранить пуллом в скрипте, в котором они цепляются и обращаться с этим (точнее говоря это по разному можно решать)

                  4. Опасно, так как вроде Vector3 не иммутабелен (поэтому его геттер и сделан через new), и если кто-то случайно в коде натупит (случайно или по невнимательности), то такую ошибку очень тяжело найти (а точнее понять в чём именно ошибка). Соглашусь, что надо кешировать и т. п., и что случай редкий, но тем не менее я бы кешировал внутри класса, где это используется. (Допустим, кто-то вызовет метод Scale или уж совсем непонятно зачем — Set)

                  5. А как же StringBuilder?

                  С этим я просто не согласен, читабельность можно сохранить при правильных подходах если не торопиться. В крайнем случае решается это комментарием, что делает эта магическая строка.
                  «Самым плохим моментом оптимизаций является то, что ваш структурированный и „идеальный“ код растекается в местами не очень читабельное нечто. К сожалению, это неизбежно. Главное помнить о том, что это жертва в угоду производительности.»


                  И очень не хватает картинок
                    0
                    1. Не поленился и создал тестовую сцену в которую добавил 50 тысяч объектов, каждый из которых в своем Update вызывает get и set на всех возможных вариантах: свойство с явно указанным полем, автосвойство, отдельно методы, и только поле.

                    Результат профайлера (мс):
                    AutoProperty 19.2
                    BackProperty 18.03
                    Methods 17.98
                    Field 2.86

                    До этого проводил похожее исследование циклом на 10 миллионов запросов к get и set, что привело к средним значениям в тиках:
                    AutoProperty ~2605162
                    BackProperty ~2535456
                    Methods ~2525719
                    Field ~777295
                    Из которых около 500000 это чистое время цикла, что в целом соответствует распределению значений выше.

                    Согласен — напрямую реализованные Свойства с явными геттерами и сеттерами практически равны по производительности явным Методам, и паритет тут в пределах погрешности. Однако всегда автосвойство проигрывает достаточно ощутимо, а поле по скорости вырывается с огромным преимуществом.

                    Я в своем проекте использовал почти везде автосвойства именно для инкапсуляции, однако увидев такую существенную разницу с полями — пришлось переходить на них.

                    2. GetComponent — да, быстрее будет складывать объекты напрямую, но в некоторых случаях, когда пишешь более общую логику, которую будешь развешивать на множество существующих объектов, есть большой соблазн забрать компонент именно через эту функцию. Однако речь в этом пункте шла не только об этой функции, но и стандартных компонентах вроде transform и rigidbody.

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

                    5. StringBuilder — это стандартное решение, и в данном случае оно все равно не спасает. В итоге необходимо все равно обращаться к ToString, что и вызовет дополнительную аллокацию. Есть грязные хаки для переиспользования памяти внутри StringBuilder, но меня даже это не спасло. Если интересно — советую поискать garbage-free-string, но для себя я не нашел среди этой информации приемлемого решения или даже намека на него.

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

                    Насчет комментариев у меня также свое мнение — удаляйте их отовсюду! Средства языка программирования достаточно выразительны, чтобы обойтись без них. Код не должен содержать магии ни под каким предлогом, кроме случая, когда это единственно возможное решение, и этот кусок каждый раз правит какой-то «добросовестный» коллега. Только для такого случая можно навесить комментарий с призывом не трогать, но если он протухнет, лично вы виноваты, в оставленном среди важных строк кода неверном мусорном комментарии.
                      0
                      Спасибо за замеры с 1. На самом деле было интересно, есть ли разница. Автосвойствами я не пользуюсь, так как пока не возникало кейсов, где они были бы нужны. В некоторых случаях можно кешировать проперти (чтобы не дёргать лишний раз), но согласен, что не всегда.

                      А про комменты тоже не соглашусь, в они часто бывают полезны, так как если скажем в коде реализован сложный алгоритм (тот же волновой алгоритм поиска пути или же алгоритм триангуляции невыпуклого контура), то не каждый с ходу их сможет узнать, и был бы полезен коммент, что тут именно происходит, чтобы не лезть в документацию лишний раз. Да, и в ряде случаев люди мыслят немного по разному и нейминг, который одному человеку кажется очевидным для другого не так очевиден.
                        –1
                        Ваши тесты может и правильные. Вы можете привести реальный пример в котором 50 тысяч объектов обновляют свойства в Update loop?
                          0
                          Построение триангуляционной сетки навмеша в рантайме для динамических препятствий с разным радиусом и произвольным положением в пространстве. Свойства не обновляются, но запрашиваются каждый раз через вызов геттера, что есть по сути обычный метод. Так вот если сделать все свойства-кишки объектов публичными и сделать внешнюю обработку (simd, data driven, называйте как угодно) — скорость возрастает на порядки.
                            –1
                            А еще это не надо делать в Update loop, а можно делать фиксированное количество раз в секунду. Оптимизировать конкретную задачу можно разными способами. Наилучший способ оптимизировать 50 тысяч вызовов свойств в Update, это не делать 50 тысяч вызовов свойств в update а не отказываться от свойств. Хотя я в вашем примере так и не увидел необходимости делать 50к вызовов свойств в каждом update цикле.
                              –1
                              А еще задачи бывают разные, например, 100 юнитов разных радиусов должны быть способными найти путь в полностью динамическом мире из сотни сфер (юниты принадлежат фракциям, каждая из которых непроходима для своих союзников), апроксимированных 5-угольниками для упрощения рассчетов. Вот все это работает порядка 70мс для каждого юнита с применением свойств и 1.5мс без них. По поводу вызовов — никто не говорил про тысячи вызовов Update, бывают итеративные / рекурсивные реализации определенных алгоритмов, где нужно много и часто щупать свойства.
                      –1
                      Почти все советы оптимизации по CPU — это реально «вредные советы», и в целом, ахинея. Лучше удалите этот бред, а то сейчас новички начитаются и пойдут лепить код без свойств, строк и foreach.

                      Вы, конечно, тут кидаетесь результатами синтетик тестов, мол, вот доступ к свойствам в 10 раз быстрее, чем доступ к полю. Только вы не учитываете, какой процент нагрузки на CPU реально занимает игровая логика, а какой — логика движка. Нет никакого смысла заранее писать код без свойств, если в 99.9999% случаев этот код будет вызываться одноразово, а не в Update. Это называется оптимизация на спичках.

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


                      С одной стороны, вы, конечно, правы. Инстанциирование объектов — это тяжелая операция. Но на моей практике использовать пул объектов мне пришлось лишь 1 раз — когда надо было удалять и инстанциировать около сотни объектов в Update. В остальных 99% случаев об этом даже не надо думать. А это утверждение, наверное, единственное адекватное из списка по CPU.

                      Мне, честно говоря, страшно представить, что вы делали в своей игре, если вам пришлось оптимизировать отказом от использования foreach, свойств и строк. Если у вас в игре сотни тысяч объектов, каждый из которых использует свойство в Update, то это не свойства медленные, это вы что-то делаете не так.
                        0
                        Ну скажем кешировать GetComponent(), а не вызывать его в Update или боже упаси FixedUpdate — это тоже здраво. Да, и аллоцировать лишнее учитывая фрагментацию и то, как устроен GC в .NET тоже такое, так как это может вызывать просто периодические просады CPU засчёт фрагментации, если бездумно аллоцировать кучу мелких объектов. А так да, во многих случаях про это даже особо думать не надо.
                          0
                          Понимаете, есть разница между тем, что говорите вы: «Кешируйте результат GetComponent() если он нужен в каждом Update цикле», и тем, что говорит автор: «Кешируйте все, что вы получаете через GetComponent<>».
                            0
                            Автор, в принципе, прав, любой кешированный линк на компонент всегда быстрее чем вызов GetComponent. Судя по тестам GetComponent просто пробегает каждый раз по списку всех компонентов, повешенных на ГО, что явно медленнее, чем найти компонент и запомнить его один раз. Вообще, есть простое правило — если нужно использовать что-то более одного раза — кешируй. Если линк на компонент не нужен в будущем и больше не нужно будет его искать — можно прочитать в локальную переменную и не кешировать.
                              +1
                              Ну это я скостил на добавку автора, что оптимизировать надо по необходимости. Я согласен, что фраза «не юзать строки» тоже звучит слишком громко, так как я юзаю строки для айдишников объектов и это никогда не вызывало проблем. Так же можно было бы сказать «храните всё в бинарниках, так как это быстрее, чем читать текстовые файлы, что позволяет уменьшать время загрузки», но с сериализацией в текстовые файлы банально удобнее работать при разработке продукта. Так как если скажем делать всё через поля в префабах, то с этим невозможно работать в команде, так как вы будете решать мержконфликты львиную долю времени.
                            0
                            Мне, честно говоря, страшно представить, что вы делали в своей игре, если вам пришлось оптимизировать отказом от использования foreach, свойств

                            Мне страшно, если вы прийдете в мобильный геймдев — игры и так с каждым годом становятся все тормознее и с все большим количеством фризов.
                            Баг с foreach — это «фича» mono, вызывающая boxing / unboxing айтема на каждой итерации, что вызывает gc memory allocation. Еще иногда вызывается аллокация на энумератор. Просто не использовать foreach — это самое простое и правильное что можно сделать.
                            Про свойства — читайте комменты выше. Про строки — аналогично, все упирается в утечку памяти в мусор, который собирается исключительно синхронно в главном потоке, полностью останавливая его — визуально выглядит как фриз / лаг / подергивание игры, иногда доходя до секунды.
                            Ынтерпрайз != геймдев, красота кода != исключительно правильная форма во всех областях применения.
                              +1
                              Мне страшно, если вы прийдете в мобильный геймдев


                              В таком случае прячьтесь под кровать — мы в компании выпустили около 15 мобильных игр и приложений, все из них — 3D, все из них отлично работают на low-end устройствах, выдавая стабильный FPS даже на самых дерьмовых девайсах. Да — с использованием строк, foreach и свойств.

                              Мне вот страшно, если вы и ваш друг прийдете в программирование со своими советами бездумно никогда не использовать строки и foreach. Проблема Unity (как и у php в свое время) в том, что из-за низкого порога вхождения начинают появляться вот такие вот кадры со своими советами и своими псевдо знаниями о том, как работает mono или gc, которые на самом деле были вычитаны из статей от таких же кадров.

                              Давайте разберем по-порядку.

                              «cg memory allocation в foreach»
                              Во-первых, это верно лишь в некоторых случаях. При работе с массивами компилятор развернет foreach в обычный for loop.
                              Во-вторых, It's not the size that matters, it's how you use it. Вопрос в том, когда это становится проблемой. На моей практике это становится проблемой, когда вы начинаете бездумно фигачить foreach с сотнями итераций в Update цикл. Опять же — проблема не в foreach а в вас.
                              В-третьих, допустим, есть действительно случаи, когда нужно сделать тяжелый foreach в каждом Update цикле, и его имеет смысл заменить на for. Такие случаи редкие, но встречаются, и иногда так имеет смысл делать. Но говорить, что «Просто не использовать foreach — это самое простое и правильное что можно сделать» — это же полный бред.

                              Тоже самое со свойствами и строками. Есть случаи, когда от них стоит отказаться. Но такие случаи крайне редки, и говорить что «имеет смысл ВООБЩЕ отказаться от строк и свойств» — это ахинея и за такие советы надо просто бить по рукам.

                                0
                                В таком случае прячьтесь под кровать — мы в компании выпустили около 15 мобильных игр и приложений

                                Ну так и есть, лаги-фризы, как и говорил, ничего нового.

                                При работе с массивами компилятор развернет foreach в обычный for loop.

                                Не читал, но осуждаю. Еще раз, mono 2.6.3 в юнити — это не актуальный компилятор из .net и сборщик мусора в том же mono — это не новый и работающий гораздо быстрее в том же располеднем .net-е. Не нужно пытаться интерполировать знания об одной реализации на все остальные. В юнити 5.5 собираются проапгрейдить компилятор до fw4.4, но не рантайм. Теоретически, это может пофиксить часть багов, но не все.
                                ВООБЩЕ отказаться от строк и свойств» — это ахинея и за такие советы надо просто бить по рукам.

                                Ахинея — использование автосвойств без ограничения доступа. По сути — просто увеличение вычислительной нагрузки без логического обоснования. Свойства нужны исключительно для вычисляемых полей и ограничения доступа.
                                  0
                                  Соглашусь по этому пункту, но добавлю, что в ряде ооооочень специфичных случаев — это бывает полезно из-за того, что с полями удобнее работать в плане рефлексии. Рефлексия — это конечно вообще очень медленно и т. п., но бывает полезно (как минимум для написания тестов).
                                  Ахинея — использование автосвойств без ограничения доступа. По сути — просто увеличение вычислительной нагрузки без логического обоснования. Свойства нужны исключительно для вычисляемых полей и ограничения доступа.
                                    +1
                                    Ну так и есть, лаги-фризы, как и говорил, ничего нового.


                                    Закончились аргументы — начались голословные утверждения с переходом на личности. Ничего нового.

                                    Если вы не понимаете, что свойства используются для инкапсуляции и если вы захотите изменить поле на свойство то вы сломаете обратную совместимость с уже скомпилированным кодом, который использует этот класс, то мне вас жаль.
                                    Ах, да, я забыл, у вас же свой геймдев, без компиляции кода в dll, без повторного использования кода.
                                      –1
                                      с переходом на личности.

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

                                      Как компиляция кода во внешнюю сборку связана с геймдевом? Условная компиляция как будет протаскиваться — двумя версиями сборок? Еще раз — ынтерпрайз подход — плохой вариант в геймдеве. К тому же есть большие проблемы с компиляцией кода внешним компилятором выше fw3.5 — на том же AOT-е, на коротинах (msil получается совершенно другой). Пример — https://github.com/sta/websocket-sharp — просто не работало во внешней сборке без подхачивания и сборки принудительно компилятором из моно.
                                        –1
                                        Вы меня извините, но я не знаю что такое «ынтерпрайз», «коротинах» и «подхачивание».
                                        Расшифровывать ваш «пацанский слэнг» мне, честно говоря, надоело.

                                        Как компиляция кода во внешнюю сборку связана с геймдевом?

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

                                          «Со-программы» — звучит приятнее? Для меня — не очень. Вообще, речь про это.
                                          подхачивание

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

                                          Ынтерпрайз — расширяемость и гибкость. Геймдев — особенности платформы и оптимизация. Эти направления редко совпадают.
                                          ынтерпрайз

                                          Это когда пытаются всех жить по понятиям GoF, нужно это или нет, повсеместный оверинженеринг, особенно этим грешат java-разработчики. На вопрос, зачем введены 3 уровня абстракции с 1 реализацией в каждом и этого не было в ТЗ, ответ один: «а вдруг потребуется!». Это противоположный конец палки о преждевременной оптимизации. Вот все песни об ООП и прочем — оттуда же. Почему-то адепты не хотят ничего знать об альтернативах, например, Data-driven programming. Но это право каждого, просто не нужно говорить, что «вы все говно, нужно писать с максимальной абстракцией и возможностью расширения везде, даже где не нужно, мы выпустили 15 игр и теперь все лезьте под лавку, кто не согласен». Нет единого верного решения, серебряной пули, в каждом конкретном случае требуется адаптированное решение.
                                            –1
                                            Просто не использовать foreach — это самое простое и правильное что можно сделать.


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


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

                                              0
                                              Так это не я не понял, похоже. Ну хорошо, для непонятливых резюмирую — foreach -> managed heap alloc -> garbage collect -> freeze: это уже обсуждалось и это «фича», те нужно знать об этом, а еще об этом. Ну и кто не хочет гадить в память — просто не будет использовать. Кто верит в синтаксический сахар и не хочет знать, как оно реализовано внутри и какие сайд-эффекты вызывает — это их право.
                                                0
                                                И поэтому foreach не нужно использовать никогда, игнорируя здравый смысл?
                                                Даже с учетом того, что
                                                1. В 99% случаев никакого прироста производительности это не даст
                                                2. В оставшихся 1% случаев требуется, как вы сами сказали, «адаптированное решение», которое не всегда упирается в foreach и garbage collection
                                                3. В Unity 5.5 обновили версию компилятора, и в будущих версиях будут обновлять версию runtime, и вы останетесь с исправленными багами но отвратительным кодом, с оптимизацией на спичках

                                                У меня один вопрос — что вы делаете в мире C#? Пишите игры на С++, а лучше сразу на ассемблере. Там никакого garbage collect, только unmanaged код, только хардкор. Зато без лагов и без фризов.
                                                  0
                                                  поэтому foreach не нужно использовать никогда… В 99% случаев никакого прироста производительности это не даст

                                                  Сходите по ссылкам что ли, там написано, к чему ведет. Подскажу, нет, это не мгновенное изменение перфоманса. Да, «мусор» накапливается и собирается не каждый фрейм, но когда накопится критическая масса — сборка идет разом. Да, сборка мусора останавливает поток. Остановка потока — 0 фпс.

                                                  В оставшихся 1% случаев требуется

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

                                                  В Unity 5.5 обновили версию компилятора, и в будущих версиях будут обновлять версию runtime

                                                  Не нужно выдавать желаемое за действительное — про апдейт рантайма не было ни слова, только подтверждение, что его не будет в 5.5 и пока нет в планах. Ну и главное — продукты нужно выпускать уже сейчас, а не надеятся на светлое будущее. Юнитеки могут обещать что-то годами, но оно не будет реализовано, баги так же правятся по несколько месяцев (последнее, что отправлял — заняло у них 3 месяца).
                                                  У меня один вопрос — что вы делаете в мире C#? Пишите игры на С++

                                                  Писал когда-то, после перехода в managed мир в 2005г обратно возвращаться не сильно хочется, слишком ленив стал с возрастом. Ну и зачем мне куда-то переходить, если я могу применять свои знания и не иметь фризов в более приятной managed среде? Заметьте, я никого никуда не посылаю, ни под лавку, ни в другой язык, ни в другую парадигму программирования — я просто говорю, что такое есть и какие есть косяки.
                                                    +1
                                                    Это наверное один из самых длинных споров про foreach на который я натыкался в последнее время. Мне конечно даже немного страшно влезать в вашу перепалку, но там где без foreach легко обойтись, лучше обойтись без foreach. При этом вы не сказали, в каких случаях foreach всё-таки необходим, и в чём его плюс то собственно. Его можно и нужно юзать в двух случаях, когда мы работаем с коллекцией, с которой без foreach работать нельзя (скажем с хешсетом) или передаём параметром скажем просто класс, который реализует интерфейс IEnumerable, если не ошибаюсь.

                                                    Если проходить форичем на эвейке какого-то одного объекта, то в целом конечно же +- плевать, но если мы работаем с коллекцией по которой можно пройтись через for, то почему бы не юзать его? Можно вообще охотится на бесов, и так как мы в юнити чаще всего живём в однопоточном мире, если мы знаем, что внутри цикла не меняется количество элементов коллекции, кешировать заранее Count, так как насколько мы помним у нас в пропертях ничего не инлайнится.

                                                    Я просто уже, если честно, потерял цель вашего спора. Излишняя оптимизация — это плохо, так как чаще всего её придётся переделывать, но если мы знаем, что в юнити на данный момент for работает лучше, чем foreach, то зачем использовать foreach? Конечно же никто не запрещает, и если очень хочется — пользуйтесь. Просто это не лучшая практика, при этом решение через for занимает всего на несколько символов больше.

                                                    Просто не надо уходить в крайности. Ещё можно похоливарить на тему того, что по хорошему можно обойтись без List, и он работает медленнее, чем массивы, так что давайте везде юзать массивы. Ребята — давайте жить дружно. Есть места где та или иная вещь критична, есть где в целом всё равно, а есть где по-другому нельзя. Зачем что-то выводить в уровень абсолюта?

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

                                                    Просто если вы знаете, что форичи аллоцируют лишнего в юнити и заставляют GC запускаться чаще — вот это уже хорошо. Так как если возникнет проблема, что игра по какой-то причине фризится, то вы знаете где это можно искать, а это главное.
                                                      0
                                                      Ещё можно похоливарить на тему того, что по хорошему можно обойтись без List, и он работает медленнее, чем массивы, так что давайте везде юзать массивы.

                                                      А зачем, если можно иметь и то и то? :)
                                                      https://github.com/Leopotam/LeopotamGroupLibraryUnity/blob/master/Collections/FastList.cs
                                                      Ну и для любителей погадить в память энумератор не реализован в принципе.
                                                        0
                                                        Еще можно почитать вот это: http://www.somasim.com/blog/2015/04/csharp-memory-and-performance-tips-for-unity/

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

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

                                                          Я и имел ввиду, что знание особенностей платформы — это главное. Фанатизм — это плохо что в одну, что в другую сторону. Использовать хорошую практику для конкретной платформы/технологии — понятное дело лучше, чем не использовать. Просто не надо уходить в крайности «ради идеи». Конечная цель всё-таки в любом случае получить стабильно и хорошо работающую программу, и хороший поддерживаемый код. Если при этом там будет плохой приём, который не влияет на производительность и читаемость, то не надо с пеной у рта избивать программистов за их незнание особенностей платформы. Программисты вообще не любят когда их бьют :)

                                                          А за статью спасибо — почитаю :)
                                                          –1
                                                          нужно взять хешсет и ходить по нему форичем, так как хешсет на поиск нам даст О(1).

                                                          Как бы это не было грустно, но Dictionary<T, ЛюбойТип>.ContainsKey работает быстрее в разы, чем HashSet.Contains.
                                    0
                                    Обидно на самом деле видеть столько негатива (и не только от вас, но и других пользователей), и попытке всех научить всему и вся. Я не сомневаюсь, что за вашими словами есть выводы, которые пришли из практического опыта. Если вы сможете помочь вывести истину, добавить полезных советов тем, кто зайдет впервые со своими проблемами — я буду только благодарен. Если ваших мыслей наберется на статью — я ее обязательно прочту!

                                    Мои слова отражают непосредственно мой опыт, и если внимательно читать статью, то я пишу о том, что все оптимизации необходимо применять только в проблемных местах. Думаете, что я на 100% последовал своим советам во всем проекте по доведению до производительного кода? Отнюдь — только там, где указал профайлер. И об этом также указано в статье. Если вы видите совет в книге — начинаете применять его везде? Сомневаюсь. Относитесь критически, у вас своя голова на плечах, и вы можете обработать поступившую информацию, попробовать что-то на практике, и для себя понять — стоит оно того или нет.

                                    Это в принципе мой первый проект под Unity, и я рассказал о том, через какие основные этапы пришлось проводить оптимизацию. И ведь мне тоже нужна помощь, и я озвучил в статье очень неприятное поведение плеера при запуске. Однако я не пошел во все статьи по Unity писать недовольные комментарии о том, что вы бы лучше про это написали, а не расписывали очевидные вещи, которые даже не всегда применимы, а в моей команде мы на такое не пойдем никогда — вы все делаете не так.

                                    Я рад, если у вас в копилке столько опыта. Делитесь и дополняйте, помогайте другим — критика тоже полезна, но аргументируйте верно, подкрепляйте своими результатами, но не пытайтесь задавить тем, что вы с таким не сталкивались, и принятые меры слишком чрезмерны. Иногда нужно знать к чему мы стремимся. В моем случае, я запустил на мобильном устройстве и получил абсолютно неприемлемый результат. Возможно, у меня все неправильно, но я попытался это исправить. К сожалению, я сейчас не вижу каких-то «золотых» решений в коде применительно к моему проекту, которые бы обеспечили запуск на мобильных платформах.
                                      +1
                                      Никакого негатива.
                                      я пишу о том, что все оптимизации необходимо применять только в проблемных местах


                                      По-моему, вы пишите:
                                      Никогда не используйте foreach
                                    0
                                    Один из «бичей» моно пропустили. Enum боксятся на любом виртуальном вызове GetHashCode(), Equals, ToString итд, что дает хороший засер хипа если вы решили использовать Enum в качестве ключа к словарю, или решили преобразовать его в строку, или сравнить.
                                    0
                                    В некоторых местах можно же обойтись без foreach и for и сделать на делегатах?

                                    private readonly List<MyClass> myList = new List<MyClass>();
                                    private Action InvokeMyFunc = delegate { };
                                    
                                    public void Add(MyClass entity)
                                    {
                                        myList.Add(entity);
                                        InvokeMyFunc += entity.MyFunc;
                                    }
                                    public void Remove(MyClass entity)
                                    {
                                        myList.Remove(entity);
                                        InvokeMyFunc -= entity.MyFunc;
                                    }
                                    
                                    public void InvokeAllMyFunc()
                                    {
                                        InvokeMyFunc();//Вместо foreach цикла
                                    }
                                    
                                    
                                    
                                    public class MyClass
                                    {
                                        public void MyFunc() { }
                                    }
                                    


                                    И мне очень интересен вопрос инициализации делегата чтоб в коде не проверять его на нулл. Почему так не делают? Инвокнуть пустой делегат дольше проверки на нулл?
                                      0
                                      Почему так не делают?

                                      Я так делаю.
                                      В некоторых местах можно же обойтись без foreach и for и сделать на делегатах?

                                      Проблема в том, что на каждую подписку будет течь память — нет возможности сохранить метод, для его вызова нужен контекст, в данном случае это инстанс класса. Чтобы такого не было, нужно сохранять инстанс класса, для этого сделать интерфейс:
                                      interface IMyAsync {
                                          void MyAsyncMethod();
                                      }
                                      

                                      и чтобы все MyClass-подписчики реализовывали его:
                                      class MyClass : IMyAsync {
                                          public void MyAsyncMethod() {
                                          }
                                      }
                                      

                                      Добавление будет просто внесением entity в лист, подписка на колбек больше не нужна. Отписка — просто удаление из списка. Нужно будет решить проблемы непротиворечивости коллекции (чтобы она был иммутабельна в процессе вызова методов), но это уже как бонус.
                                      Ну и метод вызова:
                                      public void InvokeAllMyFunc() {
                                          for (var i = myList.Count - 1; i >= 0; i--) {
                                              myList[i].MyAsyncMethod();
                                          }
                                      }
                                      


                                      Если айтемы не гоняются туда-сюда каждый фрейм — можно забить на утечки и пользовать эвенты, иначе — вот такой хак.
                                      0
                                      Прочитав статью, пришел в голову еще один совет:
                                      Не нужно каждый кадр обновлять строки на экране. Юзеру это не нужно и оптимизацию нужно бы начинать с этого а не с исследования по работе срок в памяти. Решение: считать среднее за секунду в float и выводить каждую секунду строкой в UIText.
                                        0
                                        Решение здравое, но оно не во всех случаях применимо. Для FPS — согласен, а вот таймер времени, который имеет точность в тысячные секунды, как в TrackMania, только запутает игрока, если цифры там не будут верно обновляться в реальном времени, и никакая дополнительная логика оптимизации не поможет именно этой точности.
                                          0
                                          unsafe код в помощь
                                          локаете строку и меняете содержимое

                                          var value = new string(' ', 100);
                                          fixed (char* valuePtr = value)
                                              valuePtr[0] = '1'; // ..
                                          

                                            0
                                            Не кроссплатформенно, к сожалению. Те нужно собирать весь проект с unsafe опцией.
                                              0
                                              На каких платформах не работает unsafe? Mono подерживает, IL2CPP поддерживает. Unity поддерживает флаги компиляции.
                                                0
                                                Раньше вебплеер не разрешал, сейчас не уверен, что webgl разрешает.
                                                  0
                                                  >вебплеер
                                                  он мертв, про него можно забыть как про платформу.

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

                                                  Так что unsafe код это одно из решений.

                                      Only users with full accounts can post comments. Log in, please.