>
>
>
PVS-Studio helps optimize Unity Engine …

Nikita Lipilin
Articles: 32

PVS-Studio helps optimize Unity Engine projects

With the recent update, the PVS-Studio analyzer can issue warnings to possible code optimization in Unity Engine projects. If you are wondering what kind of warnings the analyzer issues, how it understands what code to optimize, and why this is done specifically for Unity Engine, I invite you to read this article.

What does the analyzer have to offer?

Currently, PVS-Studio has 4 diagnostic rules that show how to optimize the code of Unity Engine projects:

  • V4001 indicates the code fragments in which boxing is performed;
  • V4002 finds expressions where it's better to replace string concatenations with StringBuilder;
  • V4003 detects where the variable capture by an anonymous function can be avoided;
  • V4004 shows that the code can be potentially optimized by reducing the use of "heavy" properties that create new collections each time they are accessed.

These seemingly simple diagnostic rules are based on the official recommendations from the Unity Engine documentation.

The diagnostic rules are located in the Optimization group. You can enable and disable them in the settings. By default, the diagnostic rules of this group are enabled. Please note that the diagnostic rules described here can be applied only to Unity Engine projects (you'll learn why in the next section).

The main feature of these diagnostic rules is that they issue warnings only to code that is likely to be executed frequently. Certainly, it's useful to optimize such code.

Note

Of course, we could create diagnostic rules that would issue warnings in all cases of boxing or a variable capture. However, in this case, the PVS-Studio users would have to deal with a lot of warnings. Moreover, most of them wouldn't be very interesting.

That's why we try to minimize the number of warnings that point to code fragments where optimization will not deliver tangible benefits.

What kind of code is executed frequently?

It seems simple at first glance. Unity Engine projects contain many special methods that are called frequently (e.g. Update, UpdateFixed, and others). First, PVS-Studio checks the code of these methods for possible optimizations.

However, these "primary" frequently called methods may contain different calls. Let's look at the example:

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))
      ....
  }
}

The code in the Update method is executed every frame, so there are several relevant optimizations. However, there is nothing in the Update method itself that needs to be optimized — there is just a normal assignment and method call. On the other hand, it's obvious that the code of the CheckValue method is executed just as often as the Update method.

So, it would also be useful to optimize the CheckValue method. What exactly can we optimize here?

The Equals method called on _previousValue takes an object type as an argument. Therefore, boxing is done when value is passed. To avoid boxing, just add the Equals method, which takes the ValueStruct type as an argument, to the definition of the ValueStruct structure.

By analyzing calls, PVS-Studio understands what code needs to be optimized. For the previous example, the V4001 diagnostic rule would have issued a warning indicating that, in the Update method, there is the CheckValue call where boxing is performed:

V4001. The frequently called 'Update' method contains the 'CheckValue(newValue)' call which performs boxing. This may decrease performance.

The message may not tell exactly where boxing occurred. However, the message includes information about all line numbers and file paths to which the analyzer issued the warning. For the example above, these are:

  • The line where the corresponding Update method is declared;
  • The line where CheckValue is called;
  • The line where the boxing occurs, i.e., the code where Equals is called.

Tools for viewing the analyzer reports (for example, plugins for Visual Studio, VS Code, or Rider) allow to easily jump to the code fragments that the warning is telling about. This helps understand exactly where boxing (or any other operation) that can be optimized is taking place.

Analysis depth

After reading the previous section, you may wonder: "What if the code that needs optimization is deeper?"

For example, in the Update method, the Foo method can be called, within which the Foo2 method can be called, within which the Foo3 method can be called (and so on). And then in some FooN of this call chain, boxing is performed, for example.

In this case, the analyzer will also issue a warning about the possibility of optimization. The call depth is not relevant for PVS-Studio. The only important thing is that the code should be directly or indirectly linked to the Update method or something similar.

When is it better not to issue warnings?

In the last section, we talked about methods like Update that are called very often. Can we conclude from the example that all the code in the method will be frequently executed?

Of course, we can't. The code almost always contains branches and loops. As a result, some fragments will be executed more often (or less often) than others. The analyzer tries to take this into account, but usually it's impossible to predict how often a condition will have the true value. However, there are some patterns where PVS-Studio clearly sees code that is rarely executed.

For example, the code that can be executed only when a button is pressed (i.e., when Input.GetKeyDown or GUI.Button returns true). Most likely, optimizations in such code won't yield much results. Of course, there may be exceptions to this rule, but the analyzer should still focus on the general case.

Another case is when the code performs an initialization that is done once (or at least rarely). Here's the example:

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

In this example, you can see that Initialize is called only if the field is set to false. Immediately after the call, the field is set to true. It's reasonable to assume that the Initialize method won't be executed on subsequent Update calls. Therefore, micro-optimizations within it are unlikely to yield noticeable results. So, there will be no warnings concerning performance within Initialize.

There are other cases when the analyzer avoids issuing warnings. PVS-Studio tries to show only fragments that can and should be optimized.

For example, the V4002 diagnostic rule indicates that you can use StringBuilder instead of string concatenation. However, it doesn't issue warnings to every concatenation. Instead, the diagnostic rule tracks instances where strings are added to the same variable multiple times.

Examples from real projects

As usual, we tested the diagnostic rules on various open-source projects and enhanced the analyzer based on the outcome we got. As a result, we seem to have reached the point where the analyzer gives good, unobtrusive tips on how to micro-optimize various projects.

For example, in the Daggerfall project, the V4001 diagnostic rule found several cases of boxing when the string.Format method is called. One of them is shown below:

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

The string.Format overload, which has the string.Format(string, object, object) signature, is called here. That is, the call will result in boxing, which can negatively affect performance. However, it's easy to get rid of boxing. Just call the ToString method on the mapPixelX and mapPixelY variables.

Judging by my experiments, it isn't. First, I looked at the IL — you can clearly see the 'box' commands there. Then I decided to try it at runtime — what if it's a JIT optimization?

I used the profiler built into Visual Studio to see if there was any difference between using ToString and not using it. After forcing a simple application to call string.Format a certain number of times, I saw that the number of allocations is much lower when using ToString. From this we can conclude that calling ToString on string.Format arguments definitely makes sense (for value types, of course).

GetTerrainName is called indirectly from the Update method of the StreamingWorld class. It's hard to say if GetTerrainName gets called often, but the fragment is worth noting.

Another example of suggested micro-optimizations are the V4003 warnings about variable capture in the jyx2 project:

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);
}

The anonymous functions used in these methods capture the xindex and yindex variables. Thus, each call creates an additional object, which can be easily avoided by rewriting the FirstOrDefault calls to foreach.

And in the hogwarts project, the V4002 diagnostic rule found a good place to use StringBuilder:

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] + " | ";
  }
  ....
}

The analyzer issued more warnings for this and other projects. However, I think that what I've shown so far is enough for a first demonstration. If you are curious to see what optimization tips PVS-Studio can give you for other Unity Engine projects (for example, yours), you can download the analyzer for free here.

It's also worth mentioning that we're looking for ideas for new diagnostic rules. If you have any thoughts on what would be useful to check with the analyzer, please leave your comments :).

Thank you for reading and good luck!