Комментарии 81
А где почитать инфу о том, что transform кэшируется самим unity?
Им бы стоило об этом написать большими буквами, а не мимоходом в посте на совершенно другую тему.
Я думаю многие, как и я, до сих пор не в курсе и городят свое кеширование.
Собственно, из-за этой проверки и тормозит .transform — перед тем, как вернуть кешированную переменную, он проверяет, варен ли кеш нулю, для чего лезет внутрь плюсовой части.
пруф
Судя по вот этим бенчмаркам, собственное кэширование дает прирост производительности в 15%-20%.
Если для вас действительно так критичны эти 15%-20%, то я считаю что проблему можно решить различными способами, например переопределить .transform, написать свой CachedMonoBehavior или вообще пройтись по всем transform'ам пост-процессинговым скриптом.
Но уж точно не прописывать вручную в каждом классе myTransform (а я такое видел, и в статье говорю именно об этом).
190130 — patched transform, access from local component
64640 — cached to internal field transform, access from local component
258130 — standard transform, access from external component
Еще раз — лучше самому попробовать, чем кидать ссылки на «а вот там такое написано, я буду им верить».
Допустим, вы правы, и вызов стандартного .transform в несколько раз медленнее, чем использование своего закэшированного transform.
Но любая оптимизация может иметь, а может не иметь смысл в рамках всего проекта.
Я провел несколько бенчмарков с .transform и с различными операциями над transform, 10 миллионов итерация каждая:
.transform: 0.2374664 sec
.cachedTransform: 0.0418570 sec
Transform.position: 0.5588227 sec
Transform.position (set) 0.5367007 sec
Transform.localPosition: 0.5538622 sec
Transform.rotation: 0.5620576 sec
Transform.parent: 0.5024181 sec
Transform.SetParent: 0.5636547 sec
Да, вы правы, и кэширование .transform даст вам прирост производительности в несколько раз. В абсолютной величине — это 2E-8 сек, т.е. 20 наносекунд. Но вы ведь к transform обращаетесь не просто так, что бы положить его в переменную, верно?
Практически любая операция над .transform, будь то взятие position, rotation, и уж тем более вызов методов типа SetParent, займет как минимум 50-60 наносекунд, и это в несколько раз превысит любой выигрыш, который вы получите с помощью своей оптимизации.
Т.е. даже если предположить, что у вас проект из одного Update цикла, и в этом update цикле вам надо взять transform и провести над ним всего-лишь одну операцию (например, поменять позицию), то ваша оптимизация позволит вам выиграть около 30%. Но 30% — это прирост производительности «в вакууме» — без учета работы самого цикла, без учета draw calls на ваш gameobject, без учета физики, без учета всего.
В реальной жизни, ваша оптимизация даст вам настолько мизерный прирост производительности, что в масштабах игры он будет абсолютно не значителен.
Поэтому предложение кэшировать все .transform — это оптимизация, она работает, но это оптимизация «на спичках». Если вы со мной не согласны, я предлагаю вам взять любой серьезный проект, и провести замеры производительности с кэшированием и без кэширования transform.
По поводу перерассчета самого трансформа — это другая тема, юнитеки хвастались, что подхачили их и получили ускорение в 1.5-2 раза и собирались выпустить в unity5.5. Но как обычно — слушай маркетологов из юнити и продолжай делать то, что делаешь. На данный момент планы по релизу перенесены на unity5.6.
Мне всё равно трудно представить, как у вас может быть прирост в производительности в 2-3 раза от использования cached transform, моя математика выше это опровергает.
Но даже если представить, что это реально дает вам хоть какой-то прирост производительности, даже в 10%-20% — пожалуйста. Никто же не говорит, что это никогда нельзя делать. Все зависит от конкретного случая.
Но в общем случае, в 99% проектах, я убежден, что это не даст никакого результата, по сравнению с тем, сколько процессорного времени занимают другие расчеты, вызовы функций, draw calls, тот же GC, итд.
Никто же не говорит, что это никогда нельзя делать.
Тезис в статье говорит об обратном :)
Если нужно закэшировать такие поля — обертка над MonoBehaviour это самое логичное решение. Ее можно сделать на любом этапе проекта, когда в этом действительно возникнет необходимость.
13, 14, 15 — посмотри, к примеру, видео от разработчиков Inside на Unite 2016. Свойства, foreach, Linq тормозят игру и они советую ничего из этого не использовать.
А Linq действительно много мусора создаёт.
Что бы не устраивать холивар, я предлагаю условиться, что foreach действительно создает мусор, который потом приходится собирать GC. Но во-первых, это правило работает не всегда (иногда foreach разворачивается компилятором в обычный for), а во-вторых, если не обходить большие списки в каждом кадре, в 99% случаев проблемой это не является.Так зачем тогда его использовать, если он всегда либо равен for, либо хуже?
P.S. в этом же видео и много другого интересного есть. Особенно занятно посмотреть на их кастомный профайлер.
Я думаю, что проблема в преждевременной оптимизации. Если команде нравится foreach/for/while/… — пожалуйста, пока это не пробивает производительность. Начинаются проблемы — только тогда надо микрооптимизацией заниматься. Иначе легко весь проект загубить.
Надо понимать плюсы и минусы каждого — и всё будет хорошо.
поясните, пожалуйста, пункт 7, а то вообще не понятно что вы имеете ввиду?
и пункт 12 тоже поясните: чем плохо кэшировать ссылки на компоненты?
Мне кажется в случае с API имеет смысл написать расширение редактора для работы с этим полем, которое в свою очередь так же будет работать через API.
7 пункт использую для VIEW слоя — все вьюшные компоненты засовываю в такие поля. Видел, что делают через всякие GetChild/GO.Find(), но по мне это привносит неудобства:
1) нужно сохранять именование/порядок, чтобы ничего не отвалилось
2) не понятно когда ссылка установлена
Для себя выбрал подход все что относится к ui по возможности настраивать в коде(колбеки к примеру), а на редактор оставить форматирование(стили)
— Никогда не используйте коллекции, это MS и ничего хорошего быть не может, используйте массив, там же есть возможность ресайза
— Если всё же вы решились использовать коллекции, не используйте Stack, он от лукавого, эмулируйте всё это на List, а лучше всё же на Array.
— Не изучайте математику, это вам не институт, делайте всё по наитию, другим программистам будет проще вас понять
В плане Code Style, там не только проблема с именованием, что больше всего напрягло, так это:
— Встроенные методы приватные, а никак не protected, при этом и не виртуальные, что даже здравую логику нарушает, как-то вообще непонятно как они должны вызываться
— Private не пишется
— Фигурные скобки открываются на строке с кодом
Какой-то набор вредных советов, хотя бы за коллекции можете пояснить? или "МС и ничего хорошего быть не может" на ваш взгляд хорошее объяснение?
А хотя… почитал статью и понял, что ваш коммент с ней примерно в одном стиле, нет вопросов.
Действительно :-) Но всё же такой формат как по мне не слишком понятен без объяснений почему тот или иной совет вреден.
Если всё же вы решились использовать коллекции, не используйте Stack, он от лукавого, эмулируйте всё это на List, а лучше всё же на Array.
Внутри оно все-равно сделано через List, а List — через Array. В чем прикол делать прослойки, если можно реализовать быстрее? Для прототипирования — сойдет и штатный BCL, если нужна реальная скорость на куче итераций по данным — приходится писать свое, например, симулировать List с прямым доступом к внутренним данным. Да, не безопасно, да скорость просто несоизмеримо выше при итерациях по массиву вместо индексатора List-а. Решение должно быть под задачу.
Внутри оно все-равно сделано через List, а List — через Array. В чем прикол делать прослойки, если можно реализовать быстрее? Для прототипирования — сойдет и штатный BCL, если нужна реальная скорость на куче итераций по данным — приходится писать свое
А как вообще измеряется производительность Array по сравнению с List? Есть фреймворки какие-нибудь, которые с этим помогают?
Обращение к свойству List — это get / set методы.
const int MaxIteration = 100000;
const int ItemsAmount = 10;
IEnumerator Start() {
// чтобы unity успела стартануть и меньше влияла на тест.
yield return new WaitForSeconds(1f);
var list1 = new List<int>();
var list2 = new FastList<int>();
var sw = new System.Diagnostics.Stopwatch ();
for (var i = 0; i < ItemsAmount; i++) {
list1.Add(i);
list2.Add(i);
}
int t;
sw.Reset();
sw.Start();
for (var i = 0; i < MaxIteration; i++) {
t = list1[i % ItemsAmount];
}
sw.Stop();
Debug.Log(sw.ElapsedTicks);
sw.Reset();
sw.Start();
for (var i = 0; i < MaxIteration; i++) {
t = list2[i % ItemsAmount];
}
sw.Stop();
Debug.Log(sw.ElapsedTicks);
sw.Reset();
sw.Start();
var data = list2.GetData();
for (var i = 0; i < MaxIteration; i++) {
t = data[i % ItemsAmount];
}
sw.Stop();
Debug.Log(sw.ElapsedTicks);
}
На выходе 3 циферки, надеюсь, что в порядке убывания :) Писал в редакторе хабра, так-что не уверен, то скомпилируется, но суть, думаю, ясна. FastList лежит тут, энумератор не реализован из принципа — для любителей foreach :)
Ну и в плане математики, к примеру вместо того, чтобы взять разницу 2х векторов, такого понагородят, что хоть стой хоть падай. Когда дело идёт дальше, до генерации многогранников и т.д., так вообще какая-то содомия.
И эти люди учат других из всех сил и часто являются представителями игровых компаний и т.д…
ну Array.Resize к примеру имеет смысл — меньше аллокаций\копирования, прямой контроль над разрастанием массива.
var a = new Vector2(1, 2);
var b = new Vector2(3, 4);
var c = a + b;
var a = new Vector2(1, 2);
var b = new Vector2(3, 4);
Vector c;
var c.x = a.x + b.x;
var c.y = a.y + b.y;
Можете проверить — второй вариант выигрывает всегда, чем больше итераций — тем существеннее. К сожалению в моно нет принудительного инлайна, есть только рекомендация, да и то с с 4.6. в юнити используется 2.6.3, собираются компилятор в unity5.6 (не рантайм) перевести на 4.4, что тоже ничего для инлайна не даст.
«Использовать для отладки только вывод в консоли, никаких дебагеров с брейкпоинтами»
PS
сталкивался как-то: ребята отлаживали игры только через консоль. Рассказывал про дебагер, брекпоинты, стек вызовов, но так и не получилось перевести эти термины на их язык.
и со слезами на глазах смотрел, как чуваки крэши фиксили расставляя выводы в консоли вместо того, чтобы пройтись дебагером
Я тогда был новичек в юнити, еще начал уточнять «а где точка входа в приложении? ну где можно поставить первую точку останова?» И мне не ответили, точнее отвечали всякую ахинею вместо не знаю «ну оно по разному», или «юнити сам решает, где ему начинать выполнение» ))) и тд
Я тоже пользуюсь выводом в консоль, чтобы маркировать ход выполнения программы, но для фикса нормального бага всегда дебагер
PS
У юнити есть очевидный "+" и он гениален — на нем с минимальными знаниями программирования можно писать крутые прилаги!!! Визуальный редактор, куча ассетов, интуитивно понятно кидаешь скрипт на объект и происходит магия!!! это все дико как круто!
Но «минимальными знаниями программирования» — все же часто становится его "-"
21) Используйте мутабельные структуры. Все это любят.
22) Изменяйте значения входных аргументов функции. Все это любят ещё больше.
23) Передавайте в функцию побольше всего. Функция разберётся, что из этого ей надо. Передавайте разными аргументами. Половину — булевскими. Комбинируя сочитания для удобства.
Про foreach тоже не согласен. Практика показала мне, что лучше сразу не мусорить делая игру, чем потом всё это исправлять.
Мне в своё время хватило родного руководства: https://docs.unity3d.com/Manual/index.html
Искал, выбирал себе какой-нибудь 3D движок, среди прочих наткнулся на Unity 3.0. Потыкался пару часов и понял ― оно. Следующие два вечера потратил на чтение официального руководства, благо, в отличие от предыдущих движков, оно было и подробное. Прочитал его один раз от начала до конца: что легко шло, прочитал полностью: какие-то очень специфические вещи, типа шейдеров, просмотрел по-диагонали. Стало понятно, что вообще в Unity есть, зачем надо, как оно между собой сообщается и в какую сторону смотреть, когда понадобиться сделать ту или иную вещь.
Имхо, кроме родного руководства больше ничего не надо. Можно ещё посмотреть обучающие ролики на интересующую тему: https://unity3d.com/learn/tutorials Ко всяким левым «урокам» на Ютубе отношусь скептически.
«Трёхмерный блестящий тетрис» за выходные написать можно. Готовую к выпуску в мир игру вряд ли, а рабочий прототип вполне.
Реализуйте игровые концепты, делайте стройную гибкую архитектуру, используйте паттерны по необходимости. Сделайте проект легко читаемым и поддерживаемым, а потом берите профайлеры, снимайте дампы и определяйте узкие места. А то можно подумать, что ваши велосипеды будут производительнее и продуманнее, чем вдоль и поперёк исследованные проверенные годами инструменты Microsoft.
Если ты не думаешь о производительности с момента появления идеи игры, не пишешь каждую строчку с «учётом производительности» в уме, то в конце, когда ты возьмёшься за профайлер, окажется, что для достижения приемлемой производительности надо переписать что-то в районе 100% всей кодовой базы. Это только в идеальном мире все тормоза сосредоточены в одном методе на 20 строк, который оптимизируется в последний день перед сдачей проекта за 20 минут. В реальности тормоза равномерно размазаны по всей кодовой базе, даже там, где ты их не ожидаешь.
P.S. Тоже не умею в Unity, но всё, о чём написал выше, опробовал на практике.
Моё мнение, если Unity позволяет писать на высокоуровневом языке, значит весьма бессмысленно отказываться от его достоинств. Предпочитаете производительность из коробки во вред поддерживаемости — берите низкоуровневый язык.
Безусловно, если не знать, что такое сложность алгоритма и не понимать, чем отличается dict[«key»] от list.Where(e => e.name == «key»), то можно наворочать такого, что прийдется переписывать 100% кодовой базы.
Но и оптимизировать на спичках с самого начала — тоже глупо. Строить проект на костылях, пытаясь ускорить обращение к transform в полтора раза (когда в рамках всего проекта этот вызов занимает 0.00001% процессорного времени по сравнению с другими вызовами) — это и есть оптимизация на спичках.
Если я не уверен, станет ли определенное место бутылочным горлышком или нет, я пытаюсь написать его так, что бы потом его было легко отрефакторить, без переписывания 100% кода.
Этот пункт должен звучать как: объявляй все поля и функции публичными вдруг тебе нужно будет поменять значение или вызвать функцию.
- каждый класс должен быть наследником MonoBehaviour, даже если это класс никогда не будет компонентом игрового объекта
- если тебе нужно, чтобы некоторые классы при инициализации выполняли одну и ту же логику — не делай базовый класс. Просто копипасть эту логику в каждый класс, потому что наследование, полиморфизм — это все ужасно медленно и сложно
- для доступа к объекту активно используй transform.GetChild(i). Можно даже несколько раз: transform.GetChild(2).GetChild(4).GetChild(0).GetChild(0). Если кто-то поменял иерархию объектов и все сломал — сам виноват.
- если ты вставил хак или костыль из-за производительности/бага в ОСи/бага в Юнити — не оставляй никакой комментарий на этот счет. Все всегда будут понимать, что этот костыль нужен, и никто его не уберет.
Мой вредный совет:
Всегда верьте статьями с «полезными» советами. Не тратьте время на проверку, делайте в точности так, как советуют. Авторы статей ― умные люди, они знают всё о вашем проекте: целевую платформу, жанр, все потенциально узкие места. Они даже точно знаю версию Unity, которую вы используете.
Начиная с версии 5.5 в Unity используется компилятор C# из Mono 4.6. В нём нет бага с аллокациами в foreach из-за боксинга итераторов. Да и до версии 5.5, если этот баг действительно на что-то принципиально влиял, его, имхо, было проще обходить использованием для финальных билдов другого компилятора, чем переписыванием исходного код на for. Сейчас же тем, кто переписывал foreach на for, можно начинать переписывать обратно. :)
Начиная с версии 5.5 в Unity используется компилятор C# из Mono 4.6.
sealed class NewBehaviourScript : MonoBehaviour {
readonly List<int> _list = new List<int> ();
void Start () {
for (int i = 0; i < 10; i++) {
_list.Add (i);
}
}
void Update () {
var sum = 0;
IList<int> data = _list;
foreach (var item in data) {
sum += item;
}
}
}
Пробуем, удивляемся, что не все энумераторы подхачены, возвращаемся обратно на for.
Не удивляемся. Интерфейсы ― отличный архитектурный инструмент, но и щедрый источник боксинга, причём не только внутри foreach. Ничего специфичного для Unity.
IList, ICollection, IEnumerable, IDictionary аллоцируют энумератор; List, HashSet, Queue, Stack, Dictionary ― нет.
Если выбирать для себя правило, как писать код так, чтобы минимизировать случаи, когда где-то что-то может случайно аллоцироваться, то правило «просто не использовать foreach» малоприменимо практически.
Stack, Queue, HashSet, Dictionary, LinkedList, ICollection, IDictionary, IEnumerable на for не переводятся, у них нет индексаторов. Можно перевести на while с использованием итератора, но получим тот же foreach. LinkedList разве что можно перевести на while без итератора, но у него и так нет проблем с foreach.
На цикл for можно перевести массивы (но у них нет проблем с foreach и никогда не было), List (но у него нет проблем с foreach) и IList ― вот он, единственный случай, где от for есть польза.
Т.е. выходит, что правило «не использовать foreach» существует ради одного единственного случая с IList, а в остальных случаях либо бессмысленно, либо малоприменимо.
Если аллокации почему-то важны, взамен предлагаю другое правило, ничуть не сложнее: «без особой на то причины не использовать коллекции-интерфейсы, а коллекциями-классами, напротив, пользоваться свободно».
Спасибо большое за статью, посмеялся. Почти все эти пункты тоже встретил как-то, устроившись на новую работу (2 3 4 5 6 8 10 11 13 14 15 17 20). Слава богу вредные советы хотя и не сразу, но проявили свою суть и отжили.
А, по 7-му пункту я не очень понял. Чем плохо скрывать поля, выставляемые из редактора? Или неправильно понял?
По идее вредный совет, это делать все поля для редактора public, не?
20 вредных советов по разработке игр на Unity