PVS-Studio helps optimize Unity Engine projects

pvsdev

Anastasiia Vorobeva

Posted on October 17, 2023

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.

Image description

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:

  • V4001indicates the code fragments in which boxing is performed;
  • V4002finds expressions where it's better to replace string concatenations with StringBuilder;
  • V4003detects where the variable capture by an anonymous function can be avoided;
  • V4004shows 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 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))
      ....
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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 V4002diagnostic 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 V4001diagnostic 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);
}
Enter fullscreen mode Exit fullscreen mode

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.

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 jyx2project:

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);
}
Enter fullscreen mode Exit fullscreen mode

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] + " | ";
  }
  ....
}
Enter fullscreen mode Exit fullscreen mode

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!

💖 💪 🙅 🚩
pvsdev
Anastasiia Vorobeva

Posted on October 17, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related