Unreal Engine - Data Oriented Design and the Cost of Tick
Simple changes that can can lead to big performance improvements
Introduction
When creating games in Unreal Engine, the engine code and most tutorials push developers towards an Object Oriented Design. Objects in the world derive from Actor, player characters often derive from Pawn, etc. While Object Oriented Programming is what most engineers are taught and allow for a quick creation of a set of classes that designers and other developers can manipulate, there are downsides to this approach. Monolithic classes that are difficult to change, huge class hierarchies via inheritance, and poor performance are some examples. Addressing the issues of deep inheritance hierarchies via composition using Actor Components still won’t address a lot of the performance problems of Object Oriented Programming.
Data Oriented Design isn’t a new concept; however it is infrequently seen in the games industry, especially when compared to Object Oriented Design. At a high level, Data Oriented Design is the idea that code should be structured and modeled around the data that will be transformed. This is in contrast to Object Oriented Design which structures code around an object hierarchy that is meant to model objects in the world.
Data Oriented Design is a large topic. This is an Unreal Engine specific and narrowly focused example demonstrating how a small change in mindset and code design can result in big performance gains.
Example
Here is a video of the example to be analyzed:
There are 10,000 actors in this scene, each with a static mesh component with a custom material. The material applies a world Z Offset and tints the mesh red based on parameters passed into the material instance. The Z offset is changed by using a cosine function and the red tint is changed by distance from the camera.
Actor Tick
Using a typical Object Oriented Design, here is a simple actor class that implements getting the Z Offset based on a cosine function and calculating the distance from the camera to get a red tint. The values are updated every tick. The camera location must be retrieved for all 10,000 actors even though it won’t change in the middle of a frame.
Using Unreal Insights, the time it takes to run this code per frame can be analyzed.
In a typical frame, the timer section is reporting about 8.2ms for ticking the 10,000 actors. Looking on the left side of the insights, the full cost is more clear — about 12.87ms. Dealing with a large number of actors that tick a relatively small amount of logic results in a large percentage of wasted time — about 36% in this example.
Actor Tick by Manager
While not actually an implementation of a Data Oriented Design, updating a large number of actors manually from an outside object can result in significant performance gains. As seen above, there is a lot of time spent in the management of the tick system. Here is how the same code could be executed from an outside object.
Here is the timing information for this code:
In a typical frame, the timer section is reporting 4.7ms to tick all of the actors. This is approaching half the time it takes to call the virtual Tick on the actors individually. Additionally, the total time is down from 12.87m — a savings of about 64%.
Tick Manager Data
Finally, if instead of having each individual actor be responsible for updating its data, the data is stored internally to the manager and updated, that code could look like this:
This is an example of what Data Oriented Design is as opposed to Object Oriented. It’s not perfect as the code still reaches into the actors to update their dynamic material, which in the process is going to jump all over RAM, causing continual cache misses. That being said even just being able to grab the camera location once as opposed to 10,000 times is a significant savings.
Here is the timing information for this code:
In a typical frame, the timer section is reporting 3.6ms to tick the internal stored data. This is about a 24% savings over the manager ticking the actors, and about a 72% savings from ticking all of the actors individually!
Conclusion
Data Oriented Design is a large topic. From creating code that is easily parallelized, easily vectorized to use SIMD operations and more cache efficient, the performance gains can be significant. Epic Games is working on their Data Oriented gameplay framework, Mass, which is currently experimental at the time of writing. Also, with Iris, Epic took more of a Data Oriented design. See SphereNetObjectPrioritizer as an example of how Iris determines net priority based on distance to the camera.
Example code can be found here.
Example data can be found here.
Recommended Links
GDC 2014: Mike Acton “Data-Oriented Design and C++”
In the UE 5.5 they added a new tick batching system:
- Add a new Tick Batching system for actors and components which can be enabled by setting the tick.AllowBatchedTicks cvar. When enabled, this will group together the execution of similar actor and component ticks which improves game thread performance. Also added options like ForEachNestedTick to TickFunction to better support manual tick batching (which can be faster than the new automated batching)
Seems fairly relevant to the "Actor Tick by Manager" section of this article.