Unity3D – Accessors are bad- Caching is good!
Unity3D is an amazing tool; It makes development a dream. It introduces coding to novices without limiting their capabilities. It removes the headaches from coding the core engine, 3D renderer, sound system and all of the components required for a good, stable, usable 3D engine by having them already there ready for you to use.
However, Unity is not perfect.
Whilst Unity provides all the basics, and has an integrated scripting system that is actually less scripting than you might think- and more true programming- some of it’s ease of use features contain surprising bottlenecks.
During the massive upgrades required to bring the old Red Forest prototype to where it is now, a very surprising issue was discovered. Here are two surprising bottlenecks in Unity:
1) Accessing the transform member.
2) Accessing the rigidBody member.
OK so strictly speaking, these are the same- they are accessing what Unity have called Accessors. You would think these are just predefined members pointing to the objects transform and rigidBody, however, my theory suggests that:
RigidBody rb=rigidbody;
is the same as
RigidBody rb=GetComponent<Rigidbody>();
[update: Actually Unity themselves have said that the exact code execuded is:
RigidBody rb=UnityEngine.Component.get_rigidBody();
which is exactly the same.]
I’ve done significant tests- now, take a look at the following:
Transform cachedTransform;
Vector3 velocity;
Rigidbody cachedRigidbody;
void Start() {
cachedTransform=transform;
cachedRigidbody=rigidbody;
}
void Update() {
velocity=cachedRigidbody.velocity; //This is ~ 18 times faster than...
velocity=rigidbody.velocity; //this.....
}
Those two should in theory be the same- however the top line is 18x faster.
Here’s the test class. Simply create a GameObject and attach a Rigidbody to it- then attach this script, and run.
Start us an IEnumerator to allow a few seconds to pass before the tests begin- just in case there’s any startup lag.
using UnityEngine;
using System.Collections;
public class AccessorTester : MonoBehaviour {
// Use this for initialization
IEnumerator Start () {
yield return new WaitForSeconds (1); //remove startup lag.
Transform cachedTransform = transform;
Rigidbody rb = rigidbody;
float endTime=0;
float startTime = Time.realtimeSinceStartup;
Debug.Log ("Reading Transform.");
Debug.Log ("Start Time: " + startTime);
Transform temp = transform;
for (int x=0;x<5000000;x++) {
temp=transform;
}
endTime=Time.realtimeSinceStartup;
Debug.Log("End time: "+endTime);
Debug.Log(endTime-startTime+" seconds taken for 5,000,000 accesses.");
float uncachedTime = endTime - startTime;
startTime = Time.realtimeSinceStartup;
Debug.Log ("Reading Cached Transform.");
Debug.Log ("Start Time: " + startTime);
for (int x=0;x<5000000;x++) {
temp=cachedTransform;
}
endTime=Time.realtimeSinceStartup;
Debug.Log("End time: "+endTime);
Debug.Log(endTime-startTime+" seconds taken for 5,000,000 cached accesses.");
float cachedTime = endTime - startTime;
Debug.Log ("cached is " + uncachedTime / cachedTime + " x faster.");
Debug.Log ("Reading RigidBody.");
Debug.Log ("Start Time: " + startTime);
Rigidbody temp2=rigidbody;
for (int x=0;x<5000000;x++) {
temp2=rigidbody;
}
endTime=Time.realtimeSinceStartup;
Debug.Log("End time: "+endTime);
Debug.Log(endTime-startTime+" seconds taken for 5,000,000 accesses.");
uncachedTime = endTime - startTime;
startTime = Time.realtimeSinceStartup;
Debug.Log ("Reading Cached RigidBody.");
Debug.Log ("Start Time: " + startTime);
for (int x=0;x<5000000;x++) {
temp2=rb;
}
endTime=Time.realtimeSinceStartup;
Debug.Log("End time: "+endTime);
Debug.Log(endTime-startTime+" seconds taken for 5,000,000 cached accesses.");
cachedTime = endTime - startTime;
Debug.Log ("cached is " + uncachedTime / cachedTime + " x faster.");
}
}
On my machine, running this code shows that accessing existing Unity accessors is around 18 times slower than using a cachedRigidbody. This was the difference between 128 ships running at 5fps, and 128 ships at 60fps in Red Forest.
So: Think about it- If you’re using a Unity ease-of-use accessor- consider switching to caching.
If you are starting a new project- or have the time to convert- consider extending MonoBehaviour and doing something like this:
First- the new ‘MonoBehaviour’:
//CoreBehaviour.cs
using UnityEngine;
using System.Collections;
public class CoreBehaviour : MonoBehaviour {
[HideInInspector]
public new Rigidbody rigidbody;
[HideInInspector]
public new Transform transform;
[HideInInspector]
public new Camera camera;
//etc
public virtual void Awake() {
transform = gameObject.transform;
if (gameObject.rigidbody) rigidbody = gameObject.rigidbody; //Overrides the slow rigidbody reference! (Thanks to Fabien Benoit-Koch for the tip)
if (gameObject.camera) camera = gameObject.camera;
Debug.Log ("Caching complete.");
}
}
Then an example script that extends from it. Note that Awake() is now a public override.
//CachedBehaviour.cs
using UnityEngine;
using System.Collections;
public class CachedBehaviour : CoreBehaviour{
public override void Awake () //Awake needs to override.
{
base.Awake(); //does the caching.
Debug.Log ("Awake called!");
}
void Update ()
{
//Now you can use transform / rigidbody as normal and still gain your speed boost!
}
}
Personally I like to separate the caching from the Awake function- so I do not use Awake() in my scripts, instead, I do the caching in the Awake() method of CoreBehaviour and have that call Init(), a virtual method which I use in all of my scripts. This is preferred as it feels cleaner, however if you already have a large project the method I have just shown is going to save you a bit of time!
You need to ensure you call base.Awake() otherwise no caching will occur and you’ll get a load of null references.
How’s that for a nice performance boost and minimal interference!
Unity have posted an update about Unity 5 explaining that they are in fact removing these accessors: Unity 5 API Changes and automatic script updating
Comments are closed.