Unity performance best practice with Rider, part 2
Last time, we looked at Rider’s new performance indicators for Unity, which highlight expensive operations inside performance critical contexts, such as calling GetComponent inside an Update method.
These highlights are intentionally different to traditional warnings and suggestions because there is no easy “fix”, partly because the code isn’t necessarily wrong, but also because removing an expensive operation requires either changing the semantics of your code, or rearchitecting parts of your application (e.g. avoiding the use of SendMessage completely). These indicators provide context and awareness, and it’s up to you (and your profiler) to decide how, when and even if you want to address the advice.
Of course, there are always code patterns that can be easily and safely rewritten to perform better simply by using a different API or overload, or by caching values. In this post, we’ll take a look at some of these inspections and the Quick Fixes you can use to follow Unity’s own best practices and avoid some common performance issues in Unity.
Avoiding unnecessary native code transitions
Let’s start with an easy one. Rider recognises Unity’s special event functions, which aren’t called like normal managed methods, but invoked directly from the native part of Unity’s engine, kind of like reflection. This transition from native to managed code isn’t free, although it’s a perfectly acceptable cost when the method is correctly implemented. But it’s an unnecessary overhead when the method is empty – you’re making Unity do work it doesn’t need to do. Rider will mark empty event functions as redundant, and a quick Alt+Enter will remove the whole method.
As ever, you can find more details about this warning, with links to official documentation, in the Unity Code Inspection wiki on GitHub. Or you can just use Alt+Enter to select “Why is Rider suggesting this?“. This applies to all of the inspections in this post
Another inspection that Rider has had for a while is to show a warning when you compare GameObject.tag to a string literal. Again, this code is perfectly fine, but it’s doing more work than it needs to – the tag property is implemented in native code and allocates memory each time it’s accessed, copying the string from native code into the managed world. Instead, you should call GameObject.CompareTag, which compares the method argument with the native string without any allocations. Rider will highlight this warning and give you a simple Quick Fix to rewrite the code.
In a similar manner, Rider will warn you when you’re repeatedly accessing properties that call into native code, such as Transform.localPosition. If the value hasn’t been changed, repeatedly accessing this property, and therefore repeatedly transitioning to and from native code, is an unnecessary expense. Rider also provides an Alt+Enter Quick Fix to introduce a new local variable and cache the value.
Of course, the cached value is only used up to the point where the property is assigned a new value. Furthermore, Rider also recognises when the property might change due to setting a different property. For example, setting Transform.position means that Transform.localPosition now has a different value, too, and Rider makes sure subsequent accesses do not use the now out-of-date cached value.
Avoiding string parameters
Unity has a lot of APIs that make use of “magic strings“, which tends to introduce complexities for user code, from “rename fragility” to potential performance issues. Last time, we saw how Rider highlights calls to Invoke and SendMessage as expensive operations, because the string values are used to look up methods to invoke. This time, we have another couple of examples – using strings for property lookup, and using strings for type lookup.
Let’s start with property lookup. Material, Shader and Animator all have parameters that can be modified from script with methods like GetFloat or SetBool. For convenience, these parameters are identified by name, and you pass this name to the setters and getters. However, this convenience has an overhead – these methods have to convert the string name into an integer identifier to look up the parameter, on each call.
The good news is that these identifiers are stable values and can be calculated in advance. Rider will recognise when you’re using the string overloads of these setters and getters, add a warning and a quick fix that will introduce or reuse a static field that will calculate the ID with Animator.StringToHash or Shader.PropertyToID and use this in the integer setter and getter overloads.
And so on to type lookup. Calls to the various GetComponent methods, as well as GameObject.AddComponent and ScriptableObject.CreateInstance can take a string literal type name which Unity will use to find the type. This has a couple of problems. Firstly, it’s fragile – the compiler doesn’t check the value for typos, and when renaming a type, you need to make sure you also rename the string value, or it will fail at runtime. And secondly, using a string for type lookup adds extra overhead. It’s much better to use the type directly, either by using typeof(Grid) or specifying the type in a generic argument, such as GetComponent<Grid>(). Fortunately, Rider helps with both problems.
If you use one of these methods with a string parameter, Rider will add a warning to the string parameter telling you that it’s inefficient, and a quick Alt+Enter later and you’ll be using the generic version.
But how does it know what type to use? This is where Rider helps with the rename fragility problem, too. Rider will check that the string value refers to a valid type, and make sure it derives from the correct base type – that it’s a component, behaviour or scriptable object. It will find the type by its short name, or you can use a namespace and get code completion. On top of that, you can Ctrl+Click on the string literal and navigate to the definition of the object (even into decompiled code) and the string will show up in Find Usages for that type. Even better, if you use the rename refactoring on the type, it will update the string usage as well. And of course, if it knows what type the string value refers to, it’s very easy to convert into a generic call, adding any using statements it needs
The last set of inspections we’ll look at today help avoid APIs that cause unnecessary allocations. Let’s start with Object.Instantiate. A common problem here is to use Instantiate to create a new instance of a Unity object and then set its parent transform in a separate step. Every object in a Unity scene has a transform that specifies its position, rotation and scale, relative to its parent in a transform hierarchy. Creating an object without specifying a parent transform will create a whole new root transform hierarchy, and it is very wasteful to then immediately throw this away by setting a correct parent transform and moving the object into an existing hierarchy. Rider will detect this pattern, warn you, and give you a Quick Fix to set the parent transform as part of the Instantiate method call.
Finally, Rider will warn you to avoid allocations when using physics APIs. There are a number of ray casting and collision detection static methods on the Physics and Physics2D classes that return an array of results.
Unfortunately, these methods allocate the array on each call, and this repeated allocation has a performance overhead. Since Unity 5.3, these methods have non-allocating versions that take in a pre-allocated array to return the results. Rider recognises this, adds a warning, and provides a quick fix to rewrite the call to the non-allocating version, leaving the text caret in a position to reference an existing array, or create a new one. See the code inspection documentation for more details.
Over these last two blog posts, we’ve seen how Rider can help you follow Unity’s own best practices for performance while writing your game scripts. This post showed how Rider can automate the quick wins where simply using a different method or caching some values will help improve performance, rewriting your code to fix these issues. And last time, we saw how Rider shows you where your application is making expensive operations, as you type, right in the editor. Of course, these recommendations are only half the story – make sure you’re profiling your code as well!
Download Rider now and see what Rider suggests for your own game!
Subscribe to Blog updates
Thanks, we've got you!