﻿# PVS\-Studio помогает оптимизировать проекты на Unity Engine

Недавно анализатор PVS\-Studio начал выдавать предупреждения о возможностях оптимизации кода в проектах под Unity Engine\. Какие они, эти предупреждения? Как анализатор понимает, какой код стоит оптимизировать? Почему это сделано именно для Unity? Ответы в заметке\.

![1071_Unity_optimizations_ru/image1.png](https://import.viva64.com/docx/blog/1071_Unity_optimizations_ru/image1.png)

## Что может посоветовать анализатор?

На момент написания этой статьи в PVS\-Studio есть 4 правила, указывающих на возможности оптимизации кода проектов под Unity:

* [V4001](https://pvs-studio.ru/ru/docs/warnings/v4001/) указывает на фрагменты кода, в которых производится [упаковка](https://pvs-studio.ru/ru/blog/terms/6695/);
* [V4002](https://pvs-studio.ru/ru/docs/warnings/v4002/) находит выражения, в которых конкатенации строк стоит заменить на _StringBuilder_;
* [V4003](https://pvs-studio.ru/ru/docs/warnings/v4003/) обнаруживает места, в которых можно избежать захвата переменных анонимной функцией;
* [V4004](https://pvs-studio.ru/ru/docs/warnings/v4004/) говорит о потенциальной возможности оптимизации использования "тяжёлых" свойств, создающих новые коллекции при каждом обращении\.

Эти простые на первый взгляд правила были сделаны на основе [официальных рекомендаций](https://docs.unity3d.com/Manual/performance-garbage-collection-best-practices.html) в документации к Unity Engine\.

<details>
   <summary>Как включить эти правила?</summary>

Указанные диагностические правила находятся в группе Optimization\. Их можно включать и выключать в настройках\. По умолчанию правила из этой группы **включены**\. Также стоит отметить, что описанные здесь диагностики работают исключительно на проектах под Unity Engine \(в следующем разделе станет ясно, почему\)\.


</details>
Главной особенностью этих диагностик является то, что они стараются выдавать предупреждения исключительно на код, который потенциально выполняется часто\. Очевидно, что оптимизировать такой код полезно\.


> \*\*Примечание\*\*
> 
> Конечно, мы могли бы сделать правила, которые выдавали бы предупреждения, скажем, вообще на все случаи упаковки или захвата переменных\\\. Это привело бы к тому, что пользователям PVS\\\-Studio пришлось бы разбирать просто тонны срабатываний\\\. При этом большая часть из них была бы не очень интересна\\\.
> 
> Именно поэтому мы стараемся минимизировать количество предупреждений, указывающих на места, где оптимизация точно не принесёт видимого результата\\\.

## Какой код выполняется часто?

На первый взгляд всё просто\. Проекты под Unity Engine содержат большое количество специальных методов, которые вызываются очень часто \(например, _Update_, _UpdateFixed_ и другие\)\. В первую очередь именно код в этих методах PVS\-Studio будет проверять на возможность внесения оптимизаций\.

Однако внутри этих "первичных" часто вызываемых методов тоже могут быть различные вызовы\. Пример:

```cpp
class Test : MonoBehaviour
{
  struct ValueStruct { int a; int b; }

  ValueStruct _previousValue;

  void Update()
  {
    ValueStruct newValue = ....
    
    if (CheckValue(newValue))
      ....
  }

  bool CheckValue(ValueStruct value)
  {
    if(_previousValue.Equals(value))
      ....
  }
}
```

Код в методе _Update_ выполняется каждый кадр, поэтому различные оптимизации кода для него актуальны\. Однако в самом методе _Update_ оптимизировать нечего — здесь есть лишь обычное присваивание и вызов метода\. С другой стороны, очевидно, что код метода _CheckValue_ выполняется так же часто, как и _Update_\.

Получается, что оптимизация метода _CheckValue_ также была бы полезна\. Что же можно тут оптимизировать?

<details>
   <summary>Ответ</summary>

Метод _Equals_, вызываемый у _\_previousValue_, принимает в качестве аргумента тип _object_\. Соответственно, при передаче _value_ будет произведена [упаковка](https://pvs-studio.ru/ru/blog/terms/6695/)\. Чтобы её избежать, нужно лишь добавить в определение структуры _ValueStruct_ метод Equals, принимающий в качестве аргумента тип _ValueStruct_\.


</details>


За счёт подобного анализа вызовов PVS\-Studio и понимает, какой код может нуждаться в оптимизации\. Для предыдущего примера диагностика [V4001](https://pvs-studio.ru/ru/docs/warnings/v4001/) сгенерировала бы предупреждение, указывающее, что в методе _Update_ есть вызов _CheckValue_, в котором производится упаковка:

[V4001](https://pvs-studio.ru/ru/docs/warnings/v4001/)\. The frequently called 'Update' method contains the 'CheckValue\(newValue\)' call which performs boxing\. This may decrease performance\.

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

* Строка, в которой объявлен соответствующий метод _Update_;
* Строка, на которой вызывается _CheckValue_;
* Строка, где производится упаковка, то есть место вызова _Equals_\.

Средства просмотра отчёта анализатора \(например, плагины для Visual Studio, VS Code, Rider\) позволяют легко переходить к фрагментам кода, о которых говорит предупреждение\. Это позволяет понять, где именно производится упаковка \(или другая операция\), которую можно оптимизировать\.


> \*\*Глубина анализа\*\*
> 
> После прочтения предыдущего раздела может возникнуть вопрос: А что, если код, нуждающийся в оптимизации, будет глубже?
> 
> К примеру, в методе \_Update\_ может вызываться метод \_Foo\_, внутри которого может вызываться метод \_Foo2\_, внутри которого может вызываться \_Foo3\_ \\\(и так далее\\\)\\\. И в некотором \_FooN\_ в этой цепочке вызовов производится, скажем, упаковка\\\.
> 
> В этом случае анализатор также выдаст предупреждение о возможности оптимизации\\\. Глубина вызова для PVS\\\-Studio не важна\\\. Важно лишь, чтобы код напрямую или опосредовано был связан с методом \_Update\_ или подобным ему\\\.

## В каких случаях предупреждения лучше не выдавать?

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

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

Например, код может выполняться только при нажатии какой\-то кнопки \(т\. е\. когда _Input\.GetKeyDown_ или _GUI\.Button_ возвращают _true_\)\. Скорее всего, оптимизации в таком коде не принесут больших результатов\. Безусловно, из этого правила могут быть исключения, но анализатор всё\-таки должен ориентироваться на общий случай\.

Другой кейс — когда код производит инициализацию, выполняемую единожды \(или по крайней мере, редко\)\. Пример:

```cpp
class Test : MonoBehaviour
{
  private bool _initialized;
  
  void Update()
  {
    if (!_initialized)
    {
      Initialize();
      _initialized = true;
    }
  }
}
```

В данном примере видно, что _Initialize_ вызывается, только если поле имеет значение _false_\. Сразу после вызова полю присваивается значение _true_\. Резонно предположить, что при последующих вызовах _Update_ метод _Initialize_ отрабатывать не будет\. Соответственно, микрооптимизации внутри него, скорее всего, не принесут заметных результатов\. Поэтому и предупреждений на тему производительности внутри _Initialize_ не будет\.

Есть и другие случаи, когда анализатор избегает выдачи предупреждений\. PVS\-Studio старается указывать только на фрагменты, которые действительно **можно и нужно** оптимизировать\.

К примеру, правило [V4002](https://pvs-studio.ru/ru/docs/warnings/v4002/) указывает на возможность использования _StringBuilder_ вместо конкатенации строк\. Однако оно не будет ругаться на все конкатенации подряд\. Вместо этого правило отслеживает случаи многократного добавления строк к одной и той же переменной\.

## Примеры из реальных проектов

Как обычно, мы тестировали диагностики на разных Open Source проектах и вносили коррективы в анализатор на основе получаемых результатов\. В итоге, кажется, получилось добиться выдачи неплохих ненавязчивых советов по микрооптимизациям на разных проектах\.

Например, на проекте [Daggerfall](https://github.com/Interkarma/daggerfall-unity) правило [V4001](https://pvs-studio.ru/ru/docs/warnings/v4001/) указало на ряд случаев упаковки при вызове метода _string\.Format_\. Один из них представлен ниже:

```cpp
public static string GetTerrainName(int mapPixelX, int mapPixelY)
{
  return string.Format("DaggerfallTerrain [{0},{1}]",
                       mapPixelX,
                       mapPixelY);
}
```

Здесь вызывается перегрузка _string\.Format_, имеющая сигнатуру _string\.Format\(string, object, object\)_\. Соответственно, при вызове будет производится упаковка, что может негативно сказываться на производительности\. При этом избавиться от упаковки легко — достаточно лишь вызвать у переменных _mapPixelX_ и _mapPixelY_ метод _ToString_\.

<details>
   <summary>Разве такой код не оптимизируется автоматически?</summary>

Судя по моим экспериментам, нет\. Сперва я решил просто посмотреть IL — там вполне чётко видно команды 'box'\. Потом я решил попробовать в runtime — вдруг оптимизацию выполняет JIT?

Я использовал профилировщик, встроенный в Visual Studio, чтобы проверить наличие разницы при использовании _ToString_ и без него\. Заставив простое приложение вызвать _string\.Format_ некоторое количество раз, я увидел, что количество аллокаций **при использовании** _ToString_ колоссально **меньше**\. Из этого можно сделать вывод, что вызывать _ToString_ у аргументов _string\.Format_ определённо имеет смысл \(для значимых типов, конечно\)\.


</details>


Вызов _GetTerrainName_ опосредованно производится из метода _Update_ класса [_StreamingWorld_](https://github.com/Interkarma/daggerfall-unity/blob/2650483567df57e9a8410c082d971a95d9059f97/Assets/Scripts/Terrain/StreamingWorld.cs#L38)\. Сложно сказать, действительно ли сам _GetTerrainName_ вызывается часто, но обратить внимание на фрагмент стоит\.

Ещё одним примером предлагаемых микрооптимизаций являются предупреждения [V4003](https://pvs-studio.ru/ru/docs/warnings/v4003/) о захвате переменных на проекте [jyx2](https://github.com/jynew/jynew):

```cpp
public BattleBlockData GetBlockData(int xindex, int yindex)
{
  return _battleBlocks.FirstOrDefault(x =>    x.BattlePos.X == xindex
                                           && x.BattlePos.Y == yindex);
}

public BattleBlockData GetRangelockData(int xindex, int yindex)
{
  return _rangeLayerBlocks.FirstOrDefault(x =>    x.BattlePos.X == xindex
                                               && x.BattlePos.Y == yindex);
}
```

Анонимные функции, использованные в этих методах, захватывают переменные _xindex_ и _yindex_\. Соответственно, при каждом вызове будет производиться создание дополнительного объекта, чего вполне легко можно тут избежать, переписав вызовы _FirstOrDefault_ на _foreach_\.

А в проекте [hogwarts](https://github.com/OpenHogwarts/hogwarts) правило [V4002](https://pvs-studio.ru/ru/docs/warnings/v4002/) обнаружило хорошее место для использования _StringBuilder_:

```cpp
private void OnGUI()
{
  if (!this.pView.isMine)
  {
    return;
  }

  string subscribedAndActiveCells = "Inside cells:\n";
  string subscribedCells = "Subscribed cells:\n";

  for (int index = 0; index < this.activeCells.Count; ++index)
  {
    if (index <= this.cullArea.NumberOfSubdivisions)
    {
      subscribedAndActiveCells += this.activeCells[index] + " | ";
    }

    subscribedCells += this.activeCells[index] + " | ";
  }
  ....
}
```

И на этих, и на других проектах есть ещё ряд предупреждений анализатора, но думаю, что для первой демонстрации достаточно и того, что я уже показал\. Если вам интересно посмотреть, какие советы по оптимизации даст PVS\-Studio для других проектов на Unity \(например, вашего\), то можете бесплатно загрузить анализатор [здесь](https://pvs-studio.ru/ru/pvs-studio/try-free/)\.

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

Спасибо за прочтение и удачи\!