Pull to refresh

Comments 51

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

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

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

Потому что это синтаксический сахар вокруг методов «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».
Маловато картинок. Хотя бы скриншот «мёртвого» UI остался?
По CPU возникло несколько вопросов.

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

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

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

5. А как же StringBuilder?

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


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

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

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

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

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


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

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

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


В таком случае прячьтесь под кровать — мы в компании выпустили около 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 — это самое простое и правильное что можно сделать» — это же полный бред.

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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


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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


По-моему, вы пишите:
Никогда не используйте foreach
Один из «бичей» моно пропустили. Enum боксятся на любом виртуальном вызове GetHashCode(), Equals, ToString итд, что дает хороший засер хипа если вы решили использовать Enum в качестве ключа к словарю, или решили преобразовать его в строку, или сравнить.
В некоторых местах можно же обойтись без 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() { }
}


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

Я так делаю.
В некоторых местах можно же обойтись без 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();
    }
}


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

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

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

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

Так что unsafe код это одно из решений.
Sign up to leave a comment.

Articles