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



Введение


Мы решили проверить все компоненты, библиотеки и демки, написанные на языке C#, чей исходный код предоставлен в официальном репозитории разработчиков Unity3D:

  1. UI System — система для реализации графического интерфейса.
  2. Networking — система для реализации мультиплеера.
  3. MemoryProfiler — система профилирования используемых ресурсов.
  4. XcodeAPI — компонент для взаимодействия со средой разработки Xcode.
  5. PlayableGraphVisualizer — система визуализации процесса выполнения проекта.
  6. UnityTestTools — утилиты тестирования Unity3D (без Unit тестов).
  7. AssetBundleDemo — проект, содержащий исходники AssetBundleServer'а и демонстрирующий использование AssetBundle системы.
  8. AudioDemos — проекты, демонстрирующие использование аудио системы.
  9. NativeAudioPlugins — аудио плагины (нас интересует только код, демонстрирующий их использование).
  10. GraphicsDemos — проекты, демонстрирующие использование графической системы.

Было бы очень интересно взглянуть на исходники непосредственно ядра движка, но кроме разработчиков ни у кого такой возможности пока нет. Поэтому сегодня на нашем операционном столе лишь малая часть исходных кодов движка, которые мы можем проверить. Наиболее интересными проектами для нас являются: новая UI система, предназначенная для реализации более гибкого графического интерфейса относительно старого топорного GUI, и сетевая библиотека, которая верой и правдой нам служила до появления UNet.

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

Найденные ошибки и подозрительные места


Все предупреждения, выданные анализатором, можно разделить на 3 уровня:

  1. Высокий — наиболее вероятная ошибка.
  2. Средний — возможная ошибка или опечатка.
  3. Низкий — предупреждение о маловероятно возможной ошибке или опечатке.

Мы будем рассматривать только высокий и средний уровни.

В таблице ниже представлен список проверенных проектов и итоговый результат проверки по всем проектам. Столбцы «Название проекта» и «Количество строк кода», думаю, всем понятны и не должны вызывать вопросов, а вот назначение столбца «Срабатывания анализатора» стоит объяснить. Он содержит в себе информацию о количестве срабатываний анализатора. Позитивными срабатываниями считаются те, которые прямо или косвенно указывают на ошибки или опечатки в коде. Ложные срабатывания — ложные сообщения анализаторы, которые указывают на корректные участки кода, подозревая наличие в них ошибки или опечатки. Как уже и говорилось ранее — все срабатывания разделены на 3 уровня. Мы будем рассматривать только высокий и средний, так как низкий уровень, в основном, содержит информационные сообщения или маловероятные ошибки.



По итогам проверки 10 проектов было получено 16 предупреждений высокого уровня, 75% из которых верно указали на проблемные места в коде, и 18 срабатываний среднего уровня, 39% из которых верно указали на проблемные места. Качество кода следует признать высоким, так как анализатор находит в среднем только одну ошибку на 2000 строк кода. Это хороший результат.

Итак, со статистикой мы закончили. Теперь приступим к рассмотрению непосредственно обнаруженных ошибок и опечаток.

Ошибочное регулярное выражение

V3057 Invalid regular expression patern in constructor. Inspect the first argument. AssetBundleDemo ExecuteInternalMono.cs 48

private static readonly Regex UnsafeCharsWindows = 
  new Regex("[^A-Za-z0-9\\_\\-\\.\\:\\,\\/\\@\\\\]");

При попытке создания экземпляра класса Regex с данным паттерном мы получим исключение System.ArgumentException с сообщением:
parsing \"[^A-Za-z0-9\\_\\-\\.\\:\\,\\/\\@\\]\" — Unrecognized escape sequence '

Это говорит о том, что данный паттерн является некорректным, и объект Regex не может быть создан с данным паттерном. По всей видимости, программист допустил ошибку при его проектировании.

Возможно обращение к объекту с нулевой ссылкой

V3080 Possible null dereference. Consider inspecting 't.staticFieldBytes'. MemoryProfiller CrawledDataUnpacker.cs 20


.... = packedSnapshot.typeDescriptions.Where(t => 
  t.staticFieldBytes != null & t.staticFieldBytes.Length > 0 // <=
)....

После проверки объекта на null происходит обращение к нему. При этом обращение происходит независимо от результата проверки. Это может привести к возникновению исключения NullReferenceException. Вероятнее всего программист планировал использовать оператор условного и &&, но вследствие опечатки используется оператор логического и &.

Обращение к объекту перед проверкой его на null

V3095 The 'uv2.gameObject' object was used before it was verified against null. Check lines: 1719, 1731. UnityEngine.Networking NetworkServer.cs 1719


if (uv2.gameObject.hideFlags == HideFlags.NotEditable || 
    uv2.gameObject.hideFlags == HideFlags.HideAndDontSave)
  continue;
....
if (uv2.gameObject == null)
  continue;

Сначала происходит обращение к объекту, и только потом его проверка на null. Вероятнее всего, если ссылка на объект окажется равной null, то мы получим исключение NullReferenceException, так и не дойдя до проверки.

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

  • V3095 The 'm_HorizontalScrollbarRect' object was used before it was verified against null. Check lines: 214, 220. UnityEngine.UI ScrollRect.cs 214
  • V3095 The 'm_VerticalScrollbarRect' object was used before it was verified against null. Check lines: 215, 221. UnityEngine.UI ScrollRect.cs 215

Ранее уже встречается оператор if с таким же условием, содержащий в then части безусловный оператор return

Очень интересное срабатывание, показывающее всю силу копипасты, классический пример опечатки.

V3021 There are two 'if' statements with identical conditional expressions. The first 'if' statement contains method return. This means that the second 'if' statement is senseless UnityEngine.UI StencilMaterial.cs 64


if (!baseMat.HasProperty("_StencilReadMask"))
{
  Debug.LogWarning(".... _StencilReadMask property", baseMat);
  return baseMat;
}
if (!baseMat.HasProperty("_StencilReadMask")) // <=
{
  Debug.LogWarning(".... _StencilWriteMask property", baseMat);
  return baseMat;
}

Вполне вероятно, программист скопировал часть кода и забыл изменить условия проверки после вставки.

Исходя из этой опечатки, можно сказать, что вторая проверка должна иметь вид:


if (!baseMat.HasProperty("_StencilWriteMask"))

Создание экземпляра класса исключения без дальнейшего использования

V3006 The object was created but it is not being used. The 'throw' keyword could be missing: throw new ApplicationException(FOO). AssetBundleDemo AssetBundleManager.cs 446


if (bundleBaseDownloadingURL.ToLower().StartsWith("odr://"))
{
#if ENABLE_IOS_ON_DEMAND_RESOURCES
  Log(LogType.Info, "Requesting bundle " + ....);
  m_InProgressOperations.Add(
    new AssetBundleDownloadFromODROperation(assetBundleName)
  );
#else
  new ApplicationException("Can't load bundle " + ....); // <=
#endif
}

Создается класс ApplicationException, но никак не используется. Вероятнее всего, программист хотел выбросить исключение, но забыл добавить оператор throw перед созданным исключением.

Неиспользуемые аргументы при форматировании строки

Как известно, при форматировании строк количество выражений типа {N} должно соответствовать количеству передаваемых аргументов.

V3025 Incorrect format. A different number of format items is expected while calling 'WriteLine' function. Arguments not used: port. AssetBundleDemo AssetBundleServer.cs 59


Console.WriteLine("Starting up asset bundle server.", port); // <=
Console.WriteLine("Port: {0}", port);
Console.WriteLine("Directory: {0}", basePath);

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

Цикл, который может превратиться в вечный при определенных условиях

V3032 Waiting on this expression is unreliable, as compiler may optimize some of the variables. Use volatile variable(s) or synchronization primitives to avoid this. AssetBundleDemo AssetBundleServer.cs 16


Process masterProcess = Process.GetProcessById((int)processID);
while (masterProcess == null || !masterProcess.HasExited) // <=
{
  Thread.Sleep(1000);
}

Вероятнее всего, программист хотел дождаться в цикле момента завершения внешнего процесса, но не учел, что переменная masterProcess может изначально принять значение null, если процесс не будет найден, что вызовет бесконечный цикл. Для корректной работы данного алгоритма необходимо запрашивать процесс по его идентификатору каждую итерацию цикла:


while (true) {
  Process masterProcess = Process.GetProcessById((int)processID);
  if (masterProcess == null || masterProcess.HasExited) // <=
    break;
  Thread.Sleep(1000);
}

Небезопасная инициация события

Анализатор обнаружил потенциально небезопасный вызов обработчика события (event). Возможно возникновение исключения NullReferenceException.

V3083 Unsafe invocation of event 'unload', NullReferenceException is possible. Consider assigning event to a local variable before invoking it. AssetBundleDemo AssetBundleManager.cs 47


internal void OnUnload()
{
  m_AssetBundle.Unload(false);
  if (unload != null)
    unload(); // <=
}

В данном участке кода происходит проверка на null поля unload, и затем происходит вызов данного события. Проверка на null позволит избежать исключения в случае, если на событие никто не подписан на момент его вызова.

Однако представим, что у события есть один подписчик. И в момент между проверкой на null и непосредственными вызовом обработчика события существует вероятность, что будет произведена отписка от события, например, в другом потоке. Чтобы обезопасить себя в данной ситуации можно сделать так:


internal void OnUnload()
{
  m_AssetBundle.Unload(false);
  unload?.Invoke(); // <=
}

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

Часть логического выражения всегда истинна или ложна

V3063 A part of conditional expression is always false: connId < 0. UnityEngine.Networking ConnectionArray.cs 59


public NetworkConnection Get(int connId)
{
  if (connId < 0)
  {
    return m_LocalConnections[Mathf.Abs(connId) - 1];
  }

  if (connId < 0 || connId > m_Connections.Count) // <=
  {
    ...
    return null;
  }

  return m_Connections[connId];
}

Выражение connId < 0 во второй проверке функции get всегда будет равно false, так как, используя это выражение в первой проверке, всегда производится выход из функции. Исходя из этого, во второй проверке это выражение не несет никакой смысловой и функциональной нагрузки.

Также была найдена еще одна похожая ошибка.


public bool isServer
{
  get
  {
    if (!m_IsServer)
    {
        return false;
    }

    return NetworkServer.active && m_IsServer; // <=
  }
}

Думаю, не стоит говорить о том, что данное свойство может быть легко упрощено до вида:

public bool isServer
{
  get
  {
    return m_IsServer && NetworkServer.active;
  }
}

Помимо представленных выше двух примеров, в проектах были найдены еще 6 аналогичных ошибок:
  • V3022 Expression 'm_Peers == null' is always false. UnityEngine.Networking NetworkMigrationManager.cs 710
  • V3022 Expression 'uv2.gameObject == null' is always false. UnityEngine.Networking NetworkServer.cs 1731
  • V3022 Expression 'newEnterTarget != null' is always true. UnityEngine.UI BaseInputModule.cs 147
  • V3022 Expression 'pointerEvent.pointerDrag != null' is always false. UnityEngine.UI TouchInputModule.cs 227
  • V3063 A part of conditional expression is always true: currentTest != null. UnityTestTools TestRunner.cs 237
  • V3063 A part of conditional expression is always false: connId < 0. UnityEngine.Networking ConnectionArray.cs 86

Заключение


Как и в любых других проектах, здесь не обошлось без ошибок и опечаток. Как вы могли заметить, PVS-Studio наиболее преуспел в поиске опечаток.

В свою очередь, вы также можете проверить свой, или любой другой проект, написанный на языке C/C++/C#, с помощью данного статического анализатора.

Спасибо всем за внимание! Желаю вам безбажных программ.


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Ivan Kishchenko. Discussing Errors in Unity3D's Open-Source Components.

Прочитали статью и есть вопрос?
Часто к нашим статьям задают одни и те же вопросы. Ответы на них мы собрали здесь: Ответы на вопросы читателей статей про PVS-Studio, версия 2015. Пожалуйста, ознакомьтесь со списком.
Поделиться с друзьями
-->

Комментарии (74)


  1. Nirvano
    23.08.2016 16:55

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


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


    1. mayorovp
      23.08.2016 17:10

      Там все еще хуже. "Стандарт" C# не запрещает оптимизатору использовать вместо поля локальную переменную или наоборот когда он уверен, что там одинаковые значения — если только поле не объявлено как volatile. Слово "стандарт" я взял в кавычки — потому что полного стандарта у C# нет, особенно в части модели памяти, вместо этого есть куча постов в блогах у авторов языка.


      Поэтому, формально даже код с явным "вытягиванием" в локальную переменную может работать некорректно!


      var unload = this.unload; // Оптимизатор имеет право "выкинуть" эту переменную и использовать поле
      if (unload != null) unload();

      Но, "по счастливой случайности", все известные реализации оптимизируют доступ к полям через локальные переменные, а не наоборот (иначе бы дырку быстро заткнули). По той же причине явно неправильный код может нормально работать:


      if (this.unload != null) this.unload(); // Есть шанс, что в результате оптимизации ошибка пропадет

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


      this.unload?.Invoke(); // Некуда оптимизировать


      1. Tutanhomon
        23.08.2016 17:24

        this.unload?.Invoke();
        

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


        1. mayorovp
          23.08.2016 17:38

          Ну, можно компилировать компилятором, идущим в составе Microsoft Build Tools 2015 — главное же получить бинарник, дальше версия языка не имеет значения, как и версия рантайма. Ну или самому компилятор собрать — исходный код Roslyn, кажется, открыт.


          И поскольку сам компилятор написан на C# — его можно попытаться запустить через тот же Mono...


          Но это я так, в порядке шутки.


          1. Tutanhomon
            23.08.2016 17:41
            +1

            Мсье понимает (С) ))


          1. Leopotam
            23.08.2016 17:43
            +2

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

            Абсолютно неверно. Советую попробовать скомпилировать coroutine в рантайме юнити и в рантайме C# .net и посмотреть рефлектором / испаем — абсолютно разный код. По той же причине неправильно будут работать BeginInvoke и тп штуки если их скомпилить свежим рантаймом в dll и положить в юнити. Проблема — в патченном моно из юнити.


            1. mayorovp
              23.08.2016 18:20

              Но это же не означает, что они несовместимы!


              1. Leopotam
                23.08.2016 18:31

                Они чисто условно совместимы. Попробуйте скомпилировать нечто специфическое, async / await, Task-и или что-нибудь этакое из C# — по такой логике «оно должно работать, это же C#!» :) Всегда нужно учитывать рантайм, на котором будет работать байткод. + сейчас юнитеки везде пропихивают il2cpp (с 5.4 стал официальным рантаймом для android), что тоже может повлечь за собой неадекватное поведение кода, скомпилированного не в родном для юнити окружении.
                Есть такая библиотечка — https://github.com/sta/websocket-sharp, реализующая вебсокеты поверх штатных сокетов. Ну и до юнити5 сокеты были отрублены. Ну как отрублены — их криворукие индусы в билд-процедуре проверяли типы в клиентской сборке — не начинаются ли они с System.Net.Sockets и прерывать процесс с эксепшном «насяльника, купите прошку, кушать-ма ощинь хоцца». И тут же проверяли whitelist сборок, в которых разрешены были эти типы. Теперь уже можно рассказать очень простой способ обхода этого запрета — просто берешь и делаешь неймспейс, начинающийся с Systemxxx + даешь имя сборки с таким же именем (у меня была SystematicLaziness.dll :) ) и тест проходит, те в такой сборке можно было гонять сокеты без проблем. Вот берешь эту MIT-овскую библиотечку, меняешь неймспейс, компилишь — все вроде работает в редакторе, а вот на реальных девайсах начинаются чудеса (особенно после AOT тогда еще на ios). Собственно, автор подхачил ее на совместимость с моно (проблема была в вызове асинхронных методов) и выложил на ассетстор — в реп фиксы не пошли, но можно было погуглить и найти решение.


                1. mayorovp
                  23.08.2016 20:38

                  Я компилил async / await для .NET 2.0 — сомневаюсь, что под измененный Mono это будет сложнее.


                  1. Leopotam
                    23.08.2016 20:53

                    А можно привести код, полученный рефлектором / илспаем после такой генерации в dll? Потому что это малореально в рантайме 2.0 (если только через ThreadPool и кучу сахара).


                    1. mayorovp
                      23.08.2016 20:55

                      Он ничем не отличается от кода под 4.5


                      Надо просто подсунуть нужные классы, не важно в какой сборке. Я их в то время писал сам — но сейчас советую просто вытащить нужные файлы из исходников Mono или CoreCLR


                      1. Leopotam
                        23.08.2016 20:57

                        Еще раз — в юнити mono 2.6.3 до сих пор, ни о каких .net4.5 и даже .net4 разговора не идет. Соберите в честный .net2.0 — оно не заработает.


                        1. mayorovp
                          23.08.2016 20:58

                          Заработает, если добавить в проект требуемые классы. Точно так же, как у меня все заработало в честном 2.0


                          Да, у меня в проекте был класс Task в пространстве имен System.Threading.Tasks. Ну и что?


                          1. Leopotam
                            23.08.2016 21:16

                            https://drive.google.com/open?id=0BzXJzWlUcV9lOGF5V0FTc3gzdlk — вот все, что есть в моно.


                            1. Leopotam
                              23.08.2016 21:19

                            1. mayorovp
                              23.08.2016 21:27

                              Кто из нас слепой? https://github.com/mono/referencesource/tree/mono-4.4.0-branch/mscorlib/system/threading/Tasks


                              Разумеется, я не имел в виду вытащить класс из той же самой версии Mono, которая его не имеет! Его надо вытащить в виде исходного кода из той версии, в которой этот класс есть, и положить среди своих файлов.


                              1. Leopotam
                                23.08.2016 21:28

                                Еще раз — в юнити mono 2.6.3 до сих пор

                                Уверен, что не я. Апгрейд компилятора до моно4.4 только планируется в юнити5.5, когда будет — неизвестно, возможно перенесут на 5.6 и тд.


                                1. mayorovp
                                  23.08.2016 21:30
                                  -1

                                  Да какая разница, какая там версия?! Вам что, религия не позволяет взять пару исходников из другой версии?


                                  1. Leopotam
                                    23.08.2016 21:32

                                    Зачем я буду это делать и обеспечивать себе будующий геморой с апдейтом, если текущий компилятор не поддерживает сахар с «async / await» в коде? Если без них, то можно все написать и поверх тредпула без лишних плясок.


                                    1. mayorovp
                                      23.08.2016 21:34

                                      Затем, что разговор исходно шел именно про возможность замены компилятора, на что вы возразили, что async/await не будет работать.


                                      Так вот — будет, если подложить ему кучку файликов.


                                      1. Leopotam
                                        23.08.2016 21:38

                                        Не будет — компилятор делает подмену-сахар инструкций в какие-то системные классы, аналогично работает синтсаксис linq. Нельзя заставить текущий компилятор обрабатывать ключевые слова «async / await» в коде. Если писать код в VS с обновленным компилятором, то возникнет проблема с strong-name типами (Task и прочими) — они должны будут лежать в сборке именно с тем именем / неймспейсом и подписью, что и в VS. Про подпись не уверен (моно в юнити вроде наплевать на это), но с полным путем нужных типов будут проблемы.


                                        1. mayorovp
                                          23.08.2016 21:40

                                          Я компилил async / await для .NET 2.0

                                          Я это правда делал. С именем сборки никакой проблемы не возникло — компилятор его не проверяет. Совсем не проверяет.


                                          1. Leopotam
                                            23.08.2016 21:42

                                            Если использовалась внешняя сборка с подписью, то ее имя и паблик-кей еще вроде, не уверен, нужно проверять.


                                            1. mayorovp
                                              23.08.2016 21:46

                                              Использовалась кем? Вы вообще читаете что я пишу?


                                              Я компилил async / await для .NET 2.0

                                              Откуда я в честном .NET 2.0, по-вашему, брал внешнюю сборку с подписью? Особенно учитывая, что вместо стандартных реализаций я в тот раз использовал свой велосипед?


                        1. mayorovp
                          23.08.2016 21:13

                          PS могу даже привести список классов, которых достаточно чтобы компилятор смог скомпилировать любой код, использующий async/await:


                          1. System.Threading.Tasks.Task
                          2. System.Threading.Tasks.Task<>
                          3. System.Runtime.CompilerServices.AsyncVoidMethodBuilder
                          4. System.Runtime.CompilerServices.AsyncTaskMethodBuilder
                          5. System.Runtime.CompilerServices.AsyncTaskMethodBuilder<>
                          6. TaskAwaiter — полный путь и имя не важны, этот объект должен вернуть метод Task.GetAwaiter()

                          Последние 4 типа можно сделать структурами для экономии памяти.


                          Разумеется, если копировать их из Mono — придется скопировать еще несколько, которые используются внутри. А из CoreCLR без изменений скопировать и вовсе не получится — там часть плясок вокруг ExecutionContext придется вырезать.


        1. Leopotam
          23.08.2016 17:41
          +4

          Потому что автор статьи даже не удосужился узнать особенности того, что тестирует. Конструкция "?.Invoke" появилась только в C#6, а в юнити до сих пор mono 2.6.3, что примерно соответствует C#3.5 + default-ы из C#4.
          Ну и про «а вдруг оно поменяется из другого потока» — юнити в паблик-апи вся исключительно однопоточная + внутренние проверки на вызов только из основного потока. Если кто-то решил запилить свой код в другом потоке (а это он должен сделать специально), то пусть сам и обеспечивает целостность и непротиворечивость.
          Еще автор статьи наверняка бы предложил «оптимизацию» конструкции «var t = a != null? a: b» в «var t = a ?? b», абсолютно не вникая в перегрузку операторов в кастомной реализации моно юнити (жаль, что не встретил, а то было бы совсем замечательно).
          Вывод — меньше рекламы своего поделия, больше изучения предмета тестирования.


          1. Andrey2008
            23.08.2016 17:50
            -2

            Вникать в каждый проект у нас, к сожалению, нет никаких сил.


            1. Leopotam
              23.08.2016 17:52
              +2

              Вникать в каждый проект у нас, к сожалению, нет никаких сил.

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


          1. Lailore
            23.08.2016 18:02

            Если что, если использовать Visual Stdudio и компилировать DLL который засовывается в Unity, то все прекрасно работает.


            1. Leopotam
              23.08.2016 18:05

              Почитайте еще раз, что я написал — простой линейный код почти всегда будет работать как надо, но не весь (те же coroutine-ы + всякие волшебные вещи типа принудительного boxing / unboxing enum-ключа Dictionary в foreach) + абсолютно другой код в плане размещения локальных переменных в теле циклов (приходится выносить руками чтобы не сильно падала скорость) и тд и тп.


              1. mayorovp
                23.08.2016 18:21

                А что там не так со скоростью?


                1. Leopotam
                  23.08.2016 18:33

                  Моно (который в юнити) плохо оптимизирует такое — не пытается выкидывать ненужное и не пытается выносить определения переменных за скоуп тела цикла. Если руками вынести — получаешь хорошее ускорение.


                1. Leopotam
                  23.08.2016 18:46

                  ну и с enum-ом в качестве ключа — вот такой код будет гадить в память на каждой итерации (специфично только для старых версий моно, в том числе и для юнити):

                  enum KeyType {
                      One,
                      Two
                  }
                  var dict = new Dictionary<KeyType, int>() {
                      { KeyType.One, 1},
                      { KeyType.Two, 2},
                  };
                  foreach (var pair in dict) {
                      Debug.Log(pair.key + " " + pair.value);
                  }
                  


                  1. Leopotam
                    23.08.2016 18:50

                    1. mayorovp
                      23.08.2016 20:42

                      По вашей ссылке нашел вот такое описание бага: http://www.gamasutra.com/blogs/WendelinReich/20131109/203841/C_Memory_Management_for_Unity_Developers_part_1_of_3.php, где есть вот это уточнение:


                      [EDIT] Matthew Hanlon pointed my attention to an unfortunate (yet also very interesting) discrepancy between Microsoft's current C# compiler and the older Mono/C# compiler that Unity uses 'under the hood' to compile your scripts on-the-fly. You probably know that you can use Microsoft Visual Studio to develop and even compile Unity/Mono compatible code. You just drop the respective assembly into the 'Assets' folder. All code is then executed in a Unity/Mono runtime environment. However, results can still differ depending on who compiled the code! foreach loops are just such a case, as I've only now figured out. While both compilers recognize whether a collection's GetEnumerator() returns a struct or a class, the Mono/C# has a bug which 'boxes' (see below, on boxing) a struct-enumerator to create a reference type.

                      Если в переводе на русский язык и коротко — то это баг компилятора, а не рантайма. Замена компилятора устраняет утечку памяти.


                      1. Leopotam
                        23.08.2016 20:56

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


                        1. mayorovp
                          23.08.2016 20:58

                          "У них" — да. Но мне все больше нравится идея использовать другой компилятор для своего проекта, если когда-нибудь буду его делать.


                          1. Leopotam
                            23.08.2016 21:21

                            Скоро это будет невозможно — il2cpp будет на всех платформах (десктоп оставили на десерт), т.е. моно в рантайме не будет в принципе.


                            1. mayorovp
                              23.08.2016 21:31

                              Если il2cpp будет работать в соответствии со своим названием (преобразование в cpp любого il) — это не будет проблемой.


                        1. ZimM
                          23.08.2016 21:43

                          Это с чего бы наплевать? Моно как рантайм никуда не денется, о чем Юнитеки неоднократно говорили. Исключение составляет лишь iOS, для которой обязательна возможность AOT-сборки в 64 бита, чего Mono не умеет.
                          Что меня больше напрягает — так это то, что если юнитеки повысят версию C# для рантайма — им придется долго и нудно допиливать фичи новых версий языка в il2cpp, да и то это будет неполноценно, никаких dynamic и прочих плюшек.


                          1. mayorovp
                            23.08.2016 21:54

                            Что меня больше напрягает — так это то, что если юнитеки повысят версию C# для рантайма — им придется долго и нудно допиливать фичи новых версий языка в il2cpp, да и то это будет неполноценно, никаких dynamic и прочих плюшек.

                            Ну с чего бы? Какой бы версии ни был компилятор — на выходе у него будет корректный IL. И если il2cpp оправдает свое название — то он этот самый IL "проглотит" независимо от того кто его нагенерил.


                            Тот же dynamic в конечном итоге сводится к рефлексии. Если il2cpp умеет рефлексию — сумеет и dynamic.


                            1. ZimM
                              23.08.2016 22:05

                              Толку с IL, если его нечем будет правильно выполнить? IL2CPP это не только конвертер из IL в С++, а и полноценный рантайм, у него своя реализация потоков, маршалинга и большой части стандартных классов, даже банального String. Практически уверен, что Task юнитекам придется реализовывать также самостоятельно.


                              1. mayorovp
                                23.08.2016 22:07

                                Task — это высокоуровневая абстракция, которая прекрасно реализуется в чистом управляемом коде. И у нее уже есть реализация в Mono, ничего самостоятельно делать не придется.


                          1. Leopotam
                            23.08.2016 23:21

                            il2cpp на ios — основной вроде с 5.1, il2cpp на android — основной с 5.4, il2cpp на webgl (и дальше через emscripten) — основной (с 5.4 webplayer выпилен окончательно).
                            На моно остались только десктопы, но и они скорее всего будут переведены на il2cpp. Про то, что моно останется — юнитеки говорили только про редактор. Потому и наплевать, лишь бы il2cpp в поном объеме поддерживал трансляцию.


                            1. ZimM
                              23.08.2016 23:30

                              il2cpp на android — основной с 5.4
                              Где вы видели про *основной*? С него всего лишь сняли пометку «экспериментально». Это одна из двух опций, они одинаково основные пока что. Хотя тут утверждать не буду.
                              В WebGL другого варианта, кроме IL2CPP, по сути, и нет — не портировать же Моно…

                              А вот десктопы совершенно точно будут поддерживать как il2cpp, так и Моно в качестве рантайма, и юнитеки это говорили. Пруф можно найти тут в комментах тут:
                              https://blogs.unity3d.com/ru/2014/05/20/the-future-of-scripting-in-unity/
                              The Mono JIT will remain available within the Editor and Standalone.
                              SEBASTIANO & ZIMM there are more details for this topic on the forum. Basically, the thought is IL2CPP will come to Standalone players someday; but it is not a priority and it will not replace Mono but co-exist as a build option.


                              1. Leopotam
                                23.08.2016 23:35

                                С него всего лишь сняли пометку «экспериментально»

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


                                1. ZimM
                                  23.08.2016 23:38

                                  Это было бы так, если бы они были 100% взаимозаменяемы. У Моно достаточно преимуществ перед IL2CPP (см. коммент ниже), и при этом нет каких-либо значимых недостатков (по крайней мере, для меня).


                                  1. Leopotam
                                    23.08.2016 23:43

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


                                    1. ZimM
                                      23.08.2016 23:47

                                      Это плюс, да. Хотя, IL2CPP никоим образом не помешал сообществу отреверсить Pokemon GO практически полностью :)


                                      1. Leopotam
                                        24.08.2016 02:52

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


                                        1. ZimM
                                          24.08.2016 02:56

                                          Точно. Лично колупал apk) Да и они могли ведь вполне пользоваться экспериментальной версией от 5.3.
                                          От реверсинга трафика это не спасает, конечно, но сообщество и логику работы разбирало, так что… Само собой, особо никакой защиты il2cpp не дает, просто усложняет все на порядки.


                            1. ZimM
                              23.08.2016 23:34

                              Будь моя на то воля, лично я бы использовал Mono всюду, где это возможно, по трем причинам:
                              1) Меньше итоговый размер билда.
                              2) Сборка быстрее на порядок.
                              3) AOT-компиляция автоматически лишает крутых штук типа Expression и System.Reflection.Emit, полагающихся на JIT-компиляцию.


                              1. Leopotam
                                23.08.2016 23:42

                                AOT / il2cpp билды на ios бегают быстрее на более дохлом железе, чем на более мощном, но под андроидом. Да, можно говорить о том, что платформа вся такая замечательная, оптимизированная и т.п., но и статическая типизация делает свое дело.


                                1. ZimM
                                  23.08.2016 23:45

                                  Ну я и говорю о том, что выбор рантайма зависит от проекта. В навороченной игре с кучей контента и размером билда эдак 200 МБ, пожалуй, il2cpp подойдет. Если же я делаю онлайн-пятнашки, мне даром не нужна прибавка +20 МБ к билду, когда можно всю игру запихать в 15 МБ (минимальный размер apk в 5.4.0 c Моно — примерно 10 МБ).


                                  1. Leopotam
                                    23.08.2016 23:48

                                    +20 оно дает в случае fat build, моно в этом же режиме дает +10. Не сильно большая разница, размер все-равно увеличен до 100мб на GP, а в апсторе так вообще 2гб. Другое дело, что сейчас невозможно выпилить всякий мусор типа UnityEngine.UI, UnityEngine.Networking и тп муть, которая никуда не уперлась — это все едет в билд и так же транслируется в il2cpp.


                                    1. ZimM
                                      24.08.2016 03:20

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


          1. Mingun
            23.08.2016 18:36

            Еще автор статьи наверняка бы предложил «оптимизацию» конструкции «var t = a != null? a: b» в «var t = a ?? b», абсолютно не вникая в перегрузку операторов в кастомной реализации моно юнити (жаль, что не встретил, а то было бы совсем замечательно).

            А что можно перегрузить в этом выражении? Разве тернарный оператор перегружается? И зачем это может понадобится делать?


            1. Leopotam
              23.08.2016 18:38

              Тут проблема в «a != null»: https://blogs.unity3d.com/2014/05/16/custom-operator-should-we-keep-it/
              т.е. есть вот такой удобный кейс:

              var rb = GetComponent<RigidBody>() ?? gameObject.AddComponent<RigidBody>();
              

              Вот оно будет работать совсем не так, как задумано в «классическом» поведении.


              1. Mingun
                23.08.2016 18:50

                Интересный подход, хотя, как я понял, используется только в редакторе. Так что по сути если бы анализатор указал на это место, то был бы прав. правда, это добавило бы странных глюков при отладке :) Интересно, а почему в Unity не могли переопределить и оператор ???


                1. Leopotam
                  23.08.2016 18:53

                  Так тут проблема в том, что это сделали не для всех, а только для специфических для юнити объектов типа GameObject, Component и тп + перегрузили для них «operator bool». Для штатных простых типов оно работает как задумано, те со строками, например, такое прокатит. Я и говорю — это именно специфика конкретного продукта.


                  1. Mingun
                    23.08.2016 19:22

                    Погодите. Перегрузили же не оператор приведения типа, а оператор сравнения. Это значит, что во время компиляции компилятор знает, что за объект он с null-ом сравнивает. Что мешает компилятору на этапе компиляции посмотреть, что слева от оператора ?? специальный тип и реализовать этот оператор корректно?


                    1. Leopotam
                      23.08.2016 19:27

                      Они скорее всего подхачили UnityEngine.Object и все наследники получили вот такое нестандартное поведение. Те внедрение на уровне компилятора было не сильно большое, а возможно и без внедрения обошлось (путем внутренней реализации методов этого типа при инициализации моно-рантайма) — кто знает, об этом могут рассказать только те, у кого есть полные исходники. Но в 5.5 они собираются проапгрейдить компилятор до моно 4.4, причем не полностью: https://docs.google.com/document/d/1vGzfB3gIU4AEYOjseJxjYytisFkwOKRfEzI5Db6_1vQ/edit


                    1. ZimM
                      23.08.2016 22:10

                      Ничего не мешает, в теории. Вот только по факту оператор ?? игнорирует перегрузку операторов сравнения и сравнивает через Object.ReferenceEquals.


                      1. Mingun
                        23.08.2016 22:27

                        Насколько я понял, перегрузка оператора сделана не средством языка C# (где возможно перегружать некоторые операторы), а хаком в компиляторе (иначе непонятно, что там делать в компиляторе, если сравнение и так можно перегрузить). Поэтому мне совершенно непонятно, что мешает сделать хак законченным и хакать также синтаксическую конструкцию ??, у которых тип слева входит в некий магический список классов, использовать один алгоритм сравнения, а для всех прочих — Object.ReferenceEquals.


                        1. ZimM
                          23.08.2016 22:49

                          Перегрузка сделана обычными средствами языка, в этом можно убедиться, открыв UnityEngine.Object декомпилятором.
                          Проблема не в перегрузке, а в том, что оператор ?? в С#, грубо говоря, не поддерживает перегрузку операторов. И Юнити тут ни при чем.


      1. Nirvano
        23.08.2016 21:15

        Все что вы написали — верно. Только я не понял что значит: «Там все еще хуже». Код в виде:

        this.unload?.Invoke();
        

        Развернется в:
        IL_0001: ldsfld class [mscorlib]System.Action ThirtyNineEighty.Program::Event // считываем 1 раз
        IL_0006: dup // дублируем значение
        IL_0007: brtrue.s IL_000c
        IL_0009: pop
        IL_000a: br.s IL_0012
        IL_000c: callvirt instance void [mscorlib]System.Action::Invoke()
        IL_0011: nop
        IL_0012: br.s IL_0014
        IL_0014: ret

        Что соответствует тому что я написал. За исключением того, что здесь нет дополнительной локальной переменной. А используется дублирование значения записанного на стек, после считывания поля. Но это уже детали, главное что поле считывается 1 раз, а дальше идет работа с локальным значением.


        1. mayorovp
          23.08.2016 21:20

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


          1. Leopotam
            23.08.2016 21:23

            О чем вы вообще спорите? Статья-то о тестировании чего? Пишите код в юнити-проекте, потом забирайте сборки из Temp-а и проверяйте, потому что .net компилятор гораздо прогрессивнее и агрессивнее в плане оптимизации, чем древний патченный моно.


          1. Nirvano
            23.08.2016 21:48

            Верно, что спецификация не указывает что будет. Но тем не менее, я настою на том, что практически считывание будет происходить 1 раз и в машинном коде.

            x86
            Event?.Invoke();
            01652D90 mov ecx,dword ptr ds:[400AE28h] // считываем 1 раз
            01652D96 test ecx,ecx // проверка на налл
            01652D98 jne 01652D9B
            01652D9A ret
            01652D9B mov eax,dword ptr [ecx+0Ch] // достаем копию
            01652D9E mov ecx,dword ptr [ecx+4]
            01652DA1 call eax
            01652DA3 ret


            x64
            00007FFE92E44CE0 sub rsp,28h
            00007FFE92E44CE4 mov rcx,21B1DE44C20h // считываем 1 раз
            00007FFE92E44CEE mov rcx,qword ptr [rcx]
            00007FFE92E44CF1 test rcx,rcx // проверка на налл
            00007FFE92E44CF4 jne 00007FFE92E44CFB
            00007FFE92E44CF6 add rsp,28h
            00007FFE92E44CFA ret
            00007FFE92E44CFB mov qword ptr [rsp+20h],rcx // достаем копию
            00007FFE92E44D00 mov rcx,qword ptr [rcx+8]
            00007FFE92E44D04 mov rax,qword ptr [rsp+20h]
            00007FFE92E44D09 call qword ptr [rax+18h]
            00007FFE92E44D0C nop
            00007FFE92E44D0D add rsp,28h
            00007FFE92E44D11 ret


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


            1. mayorovp
              23.08.2016 22:01
              -1

              Вот такие же "практики, а не теоретики" и попадаются теперь на постоянных UB в C++...


  1. icepro
    23.08.2016 23:37

    Странно что «Неиспользуемые аргументы при форматировании строки» позиционируется как средний или высокий уровень предупреждения.
    Как по мне то тут ничего страшного нет. Падать не будет. Неправильного поведения тоже не ожидается.


    1. Andrey2008
      24.08.2016 00:01
      +2

      Уровень у нас, это не только критичность ошибки, но и её достоверность. Конечно, это два совершенно разных понятия. Но приходится как-то объединять эти две разные характеристики в одну. Двухмерную шкалу измерения ошибки пользователи не оценят :). Здесь высокая достоверность, что это ошибка. Вот и первый уровень.


  1. FrozenTwilight
    24.08.2016 14:48

    Здравствуйте. Было бы интересно узнать как обстоят дела с кодом у OGRE 3D.


  1. zartdinov
    24.08.2016 14:48

    Да, возможно, кому-то как и мне будет радостно узнать, что актуальные версии редактора есть под linux.
    Пробовал запускать демки на Linux Mint, очень круто, просто дальше их сайта никогда не заглядывал.