Event Performance: C# vs. UnityEvent
Unity programmers have their choice of two kinds of events. We could use the built-in C# event
keyword or Unity’s UnityEvent
classes. Which is faster? Which one creates more garbage? Today’s article finds out!
First off, let’s look at how you use C# events:
class MyClass { // Declare an event for users to add their listeners to event Action<int,int> OnClick; void Foo() { // Call Invoke() to call the listeners OnClick.Invoke(11, 22); // Alternatively, call the event like a function OnClick(11, 22); } } // Use += to add a listener function to be called when the event is dispatched var myc = new MyClass(); myc.OnClick += (x, y) => Debug.LogFormat("clicked at {0}, {0}", x, y);
Overall, it’s simple and built into the language. Now let’s look at Unity’s UnityEvent
class:
// Use the namespace with UnityEvent in it using UnityEngine.Events; // Make a class extending UnityEvent, mark it [Serializable] [Serializable] class Int2Event : UnityEvent<int, int> { } class MyClass { // Declare and create an event for users to add their listeners to Int2Event OnClick = new Int2Event(); void Foo() { // Call Invoke() to call the listeners OnClick.Invoke(11, 22); } } // Use AddListener to add a listener function to be called when the event is dispatched var myc = new MyClass(); myc.OnClick.AddListener((x, y) => Debug.LogFormat("clicked at {0}, {0}", x, y));
The strange part here is the requirement that you create your own class extending UnityEvent
. You can skip this if you don’t have any parameters. Otherwise, you end up making empty, [Serializable]
classes for each event. Other than this, using these events is very much like using C# events.
Now let’s test C# events against UnityEvent
. First up, let’s see how much memory is allocated by adding listeners to each kind of event. Here’s a little script that tests that:
using System; using UnityEngine; using UnityEngine.Events; public class TestScript : MonoBehaviour { event Action csharpEv0; UnityEvent unityEv0 = new UnityEvent(); void Start() { AddCsharpListener(); AddUnityListener(); AddCsharpListener2(); AddUnityListener2(); AddCsharpListener3(); AddUnityListener3(); AddCsharpListener4(); AddUnityListener4(); } void AddCsharpListener() { csharpEv0 += NoOp; } void AddUnityListener() { unityEv0.AddListener(NoOp); } void AddCsharpListener2() { csharpEv0 += NoOp; } void AddUnityListener2() { unityEv0.AddListener(NoOp); } void AddCsharpListener3() { csharpEv0 += NoOp; } void AddUnityListener3() { unityEv0.AddListener(NoOp); } void AddCsharpListener4() { csharpEv0 += NoOp; } void AddUnityListener4() { unityEv0.AddListener(NoOp); } static void NoOp(){} }
If you open the Profiler pane in the Unity Editor, enable Deep Profile mode, then run the script you’ll get these results:
Num Listeners | C# Event Total GC Alloc | UnityEvent Total GC Alloc |
---|---|---|
1 | 104 | 192 |
2 | 416 | 320 |
3 | 812 | 448 |
4 | 1332 | 576 |
C# events start out allocating less garbage than UnityEvent
, but start allocating more as soon as you add a second listener. The gap widens more and more as you add more listeners, but those cases are less frequent in real-world programming. If you typically have zero or one listeners, C# events will create less garbage. If you have more, then UnityEvent
will allocate less garbage.
How does that picture change as we dispatch the events? To test that, here’s a tiny script that dispatches each with two listeners added:
using System; using UnityEngine; using UnityEngine.Events; public class TestScript : MonoBehaviour { event Action csharpEv0; UnityEvent unityEv0 = new UnityEvent(); string report; void Start() { csharpEv0 += NoOp0; csharpEv0 += NoOp0; unityEv0.AddListener(NoOp0); unityEv0.AddListener(NoOp0); DispatchCsharpEvent(); DispatchUnityEvent(); } void DispatchCsharpEvent() { csharpEv0.Invoke(); } void DispatchUnityEvent() { unityEv0.Invoke(); } static void NoOp0(){} }
Again using the Profiler pane in the Unity Editor we get these results:
When dispatched, C# events create no garbage whatsoever but UnityEvent
creates 136 bytes. C# events are the clear winner in this regard.
Update: UnityEvent
only creates garbage on the first dispatch. Subsequent dispatches create no garbage.
Finally, let’s test the performance of dispatching a bunch of events. It takes quite a few to get quality results, so this test dispatches 10 million of them with zero, one, or two arguments to 1-5 listeners.
using System; using UnityEngine; using UnityEngine.Events; public class TestScript : MonoBehaviour { const int NumReps = 10000000; const int MaxListeners = 5; const int MaxArgs = 3; [Serializable] class IntEvent1 : UnityEvent<int> { } [Serializable] class IntEvent2 : UnityEvent<int,int> { } event Action csharpEv0; event Action<int> csharpEv1; event Action<int,int> csharpEv2; UnityEvent unityEv0 = new UnityEvent(); IntEvent1 unityEv1 = new IntEvent1(); IntEvent2 unityEv2 = new IntEvent2(); string report; void Start() { var stopwatch = new System.Diagnostics.Stopwatch(); var csharpEvTimes = new long[MaxArgs,MaxListeners]; var unityEvTimes = new long[MaxArgs,MaxListeners]; for (var numListeners = 0; numListeners < MaxListeners; ++numListeners) { csharpEv0 += NoOp0; stopwatch.Reset(); stopwatch.Start(); for (var j = 0; j < NumReps; ++j) { csharpEv0.Invoke(); } csharpEvTimes[0,numListeners] = stopwatch.ElapsedMilliseconds; unityEv0.AddListener(NoOp0); stopwatch.Reset(); stopwatch.Start(); for (var j = 0; j < NumReps; ++j) { unityEv0.Invoke(); } unityEvTimes[0,numListeners] = stopwatch.ElapsedMilliseconds; } for (var numListeners = 0; numListeners < MaxListeners; ++numListeners) { csharpEv1 += NoOp1; stopwatch.Reset(); stopwatch.Start(); for (var j = 0; j < NumReps; ++j) { csharpEv1.Invoke(11); } csharpEvTimes[1,numListeners] = stopwatch.ElapsedMilliseconds; unityEv1.AddListener(NoOp1); stopwatch.Reset(); stopwatch.Start(); for (var j = 0; j < NumReps; ++j) { unityEv1.Invoke(11); } unityEvTimes[1,numListeners] = stopwatch.ElapsedMilliseconds; } for (var numListeners = 0; numListeners < MaxListeners; ++numListeners) { csharpEv2 += NoOp2; stopwatch.Reset(); stopwatch.Start(); for (var j = 0; j < NumReps; ++j) { csharpEv2.Invoke(11, 22); } csharpEvTimes[2,numListeners] = stopwatch.ElapsedMilliseconds; unityEv2.AddListener(NoOp2); stopwatch.Reset(); stopwatch.Start(); for (var j = 0; j < NumReps; ++j) { unityEv2.Invoke(11, 22); } unityEvTimes[2,numListeners] = stopwatch.ElapsedMilliseconds; } report = "Num Args,Num Listeners,C# Event Time,UnityEvent Time\n"; for (var i = 0; i < MaxArgs; ++i) { for (var j = 0; j < MaxListeners; ++j) { report += i + "," + (j+1) + "," + csharpEvTimes[i,j] + "," + unityEvTimes[i,j] + "\n"; } } } void OnGUI() { GUI.TextArea(new Rect(0, 0, Screen.width, Screen.height), report); } static void NoOp0(){} static void NoOp1(int a){} static void NoOp2(int a, int b){} }
If you want to try out the test yourself, simply paste the above code into a TestScript.cs
file in your Unity project’s Assets
directory and attach it to the main camera game object in a new, empty project. Then build in non-development mode for 64-bit processors and run it windowed at 640×480 with fastest graphics. I ran it that way on this machine:
- 2.3 Ghz Intel Core i7-3615QM
- Mac OS X 10.11.2
- Apple SSD SM256E, HFS+ format
- Unity 5.3.1f1, Mac OS X Standalone, x86_64, non-development
- 640×480, Fastest, Windowed
And here are the results I got:
Num Args | Num Listeners | C# Event Time | UnityEvent Time |
---|---|---|---|
0 | 1 | 30 | 206 |
0 | 2 | 89 | 306 |
0 | 3 | 151 | 406 |
0 | 4 | 206 | 514 |
0 | 5 | 272 | 612 |
1 | 1 | 33 | 685 |
1 | 2 | 91 | 807 |
1 | 3 | 151 | 980 |
1 | 4 | 212 | 1096 |
1 | 5 | 274 | 1224 |
2 | 1 | 30 | 1187 |
2 | 2 | 102 | 1371 |
2 | 3 | 172 | 1547 |
2 | 4 | 226 | 1709 |
2 | 5 | 296 | 1879 |
C# events beat UnityEvent
for each number of arguments. The gap widens as more args are dispatched to the listeners. In the best case, UnityEvent
takes 2.25x longer than C# events but in the worst case it takes almost 40x longer!
In conclusion, UnityEvent
creates less garbage than C# events if you add more than two listeners to it, but creates more garbage otherwise. It creates garbage when dispatched (Update: the first time) where C# events do not. And it’s at least twice as slow as C# events. There doesn’t seem to be a compelling case to use it, at least by these metrics. If you know of a good reason why you might, please leave a comment to let me know!
#1 by Clark on January 25th, 2016 ·
I am still on the Web and unity is no good for me. I miss being able to apply your blog articles to my understanding like the old Flash days but I still enjoy reading. Keep up the great work mate.
#2 by jackson on January 25th, 2016 ·
Thanks. :)
I’m sure you know this already, but Unity has support for HTML5 output now so you can have the best of both worlds. :)
#3 by jim on January 25th, 2016 ·
This is brilliant. Thanks for sharing
#4 by Walker on January 25th, 2016 ·
I think Unity’s new(er) input system uses UnityEvent, so you might be forced to use it when tying into that. Also, most of the stuff that Unity implements works across languages. So I would bet that UnityEvents dispatched from C# can be received in UnityScript and Boo whereas that’s harder to do with C# events. That’s totally a guess on my part, though.
Ultimately, I don’t find either of those to be compelling. They’re just the only reasons I can think to do it.
#5 by jackson on January 25th, 2016 ·
The inspiration for this article is actually working with the new Unity GUI system. :) And yes, after some more thought I figured it was for cross-language compatibility. Hopefully
UnityEvent
can be optimized in a future version to gain the performance back.#6 by focus on January 25th, 2016 ·
public UnityEvent variable very nicely exposes to the inspector so you easily may subscribe for it right from the editor.
That’s the main and probably single advantage of UnityEvent.
If you don’t wish to expose event, use built-in version.
#7 by jackson on January 25th, 2016 ·
Good point! It’s just a shame that it comes with such performance and GC penalties.
#8 by oj on February 9th, 2016 ·
Great to see some performance benchmarks for Unity Event. I also suspect that there is a big performance difference between Listeners that are hooked up in the Editor vs Listeners that are added via Code (like you did). To be used in the editor Unity Events have to be serialized (which explains why you have to create a derived class, the serializer doesn’t support generic fields). So I think that for the Editor (Persistent) Event Listener the target object reference and method info are serialized and called via reflection when the event is invoked, whereas when added via code there is a just a delegate registered.
#9 by vlepa on February 14th, 2016 ·
If you follow the Unity paradigm of component-driven architecture, there is potentially a very VERY large number of listeners of the same event. OnUnitHit, for example, can easily have 5 listeners. Imagine having 40ish units on the scene each with 5 listeners. This can happen when you seriously decouple things on the Unit. Having 5-6 scripts rather than 1 that handles everything, each might listen to a bunch of different gameplay events.
#10 by frank28 on July 21st, 2016 ·
Serializable is the only reason to use UnityEvent.
The augmented GC you observed when you subscribe new listener to C#’s event, is actually caused by old version of Mono runtime used by Unity, just like Foreach’s problem. Shame that Unity won’t upgrade Mono runtime recently.
#11 by Stephen Hodgson on September 27th, 2016 ·
Hi!
Great work on this. Any chance you could also compare C#’s events, UnityEvents, and GameObject.SendMessage() as well?
#12 by jackson on September 27th, 2016 ·
Sounds like a great idea for a followup article!
#13 by Arun on November 15th, 2016 ·
Responding to frank28:
>Serializable is the only reason to use UnityEvent.
The fact that you don’t need to unsubscribe (i.e. Unity events only provide weak references to listeners) is a big benefit for me; I use UnityEvents for some network-related updates and it can be a pain to keep track of what listeners are subscribed to what events sometimes, especially if you can’t simply hook them up in the OnEnable() / OnDisable() Monobehaviour hooks.
Am I wrong here? Does anyone else use UnityEvents primarily for that benefit?
Thanks,
Arun
#14 by Adriano Di Giovanni on June 25th, 2017 ·
Docs clearly state that references to listeners are not weak. Do you agree?
https://docs.unity3d.com/Manual/UnityEvents.html
#15 by Julian on April 26th, 2019 ·
Indeed, it seems they are _not_ weak references:
> `UnityEvents` have similar limitations to standard delegates. That is, they hold references to the element that is the target and this stops the target being garbage collected.
#16 by Fred on June 9th, 2017 ·
Hi Jackson,
I’m trying to grasp some of the knowledge you’re sharing here, so first of all thank you.
One way that I see Unity Events being used inside gameplay is to avoid the overuse of GetComponent every time you perform, say, a raycast or trigger/collider operation. So you might wanna keep a collection of listeners to improve your response when it comes to raycast or collider interaction, etc.
I guess that leads to another test, GetComponent vs Invoking events(with how many listenners)?
I have a feeling that events might be cheaper to use, in regards to having state machines, behavior trees, anything that requires instant changes on Update, since you would cache the result on the physics step(with the collisions, raycast) ,but keep track of the state changes on Update. That would sync the variables. Am I missing something?
#17 by Fred on June 9th, 2017 ·
https://en.wikipedia.org/wiki/Observer_pattern
#18 by jackson on June 9th, 2017 ·
Hey Fred,
Yes, the observer pattern is basically what events (C# and Unity) are meant to help with. Observing a
MonoBehaviour
is common, too. For example, theButton
script has an onClick event that your code can use to register that it be called when the button is clicked.I’m not sure about a comparison between events and
GetComponent
though. You could easily cache theMonoBehaviour
reference returned byGetComponent
instead of calling it repeatedly.#19 by CarlEmail on October 27th, 2018 ·
Thank for these numbers. Does the observations still hold true in the newest versions of Unity (2018.3)?
#20 by jackson on October 31st, 2018 ·
I haven’t re-tested in a while, but if you’re interested the code is all in the article so it should be easy to re-test on your target devices with your target version of Unity.
#21 by Justin Wasilenko on December 10th, 2018 ·
I was interested in testing this as well in the latest version of Unity and the difference between Mono and IL2CPP.
Here were my results:
With Mono:
C#Event Ticks 178
UnityEvent Ticks 1482
With IL2CPP:
C#Event Ticks 506
UnityEvent Ticks 1577
Tested on Windows Standalone 640×480, Fastest, Windowed, Unity 2018.3.0f1
#22 by sandeep.Nsk on January 3rd, 2019 ·
Looks like C# events are great evenin unity 2018.x
#23 by Chris on December 13th, 2018 ·
Hi Jackson,
Very interesting, thanks. I have an application where i want to improve the speed of the Monobehaviour Update calls. The problem is described here:
https://blogs.unity3d.com/2015/12/23/1k-update-calls/
So I made a gameobject with an C# action and invoking it in its Update method. Every other gameobject can attach to this event instead of implementing its own Update method. The number of listeners changes constantly. Does the action build up an array with a bigger capacity and reuses the memory?
Does this create memory garbage? Is there a better solution?
Sorry, I cannot investigate it myself right now.
Thanks,
Chris
#24 by jackson on December 15th, 2018 ·
Hi Chris,
Yes, the
Action
delegate will internally need to create an array of listeners. This is a managed array, so it will eventually result in garbage collection. There are many other possible solutions, but none are strictly better. For example, you could create your own “event” type using interfaces. Something like this:This example avoids delegates and instead imposes different restrictions. The big one is that subscribing and unsubscribing during dispatch aren’t allowed here, which simplifies and improves the performance of dispatching.
This is just an example of the many ways you can create an event system. I recommend profiling your game with the
Action
system you’ve built to get a better idea of whether you’re hitting your performance goals. If you’re not, try to find out why not and alleviate that specific problem.Best,
-Jackson
#25 by moronicAntiCoder on March 5th, 2019 ·
Lemme see if I got this right:
To use this, each object wanting to add a listener needs to interface with Ilistener and override and implement Execute(){with its dreamy stuff in here}
And somewhere else, when I want all subscribers to be called, I need to call Event.Dispatch()
#26 by jackson on March 5th, 2019 ·
Yes, that’s right. Here’s an example of using C# delegates:
And here’s an example of using this
Event
class:Syntactical differences aside, usage is very similar in many cases.
#27 by Khalid on April 3rd, 2019 ·
@Jackson you are doing great job. A big fan of your work. Thanks
#28 by xavier on February 20th, 2020 ·
One difference :
Registering multiple times the same instance callback.
C# will call multiple times the same callback.
Unity will call it only once.
Can be useful if handling conditional start/enable/destroy/disable of scripts.
#29 by Violette on April 3rd, 2020 ·
An important distinction is that UnityEvents can be linked in the editor while C# cannot. If your pipeline involves designers messing around in the editor, this can save a lot of time if they do not need to ask a programmer every times they want to link something. If, on the other side, your team consists only of programmers or your designers only stick to writing the GDD, then that’s a moot point.
#30 by Ojuergen on June 18th, 2020 ·
Performance is an important aspect of a design decision. This is a great overview, thanks!
There are some aspects that have not been mentioned, though (I think).
Even without non-coder designers in the team, there is an architectural advantage of UnityEvent over C# events, as you do not need to hard code references of one component to the other. This allows better separation of components. The approach becomes particularly interesting with nested prefabs and prefab variants.
A disadvantage of UnityEvent is code navigation. Even ReSharper does not properly detect method calls from serialized UnityEvents. This makes it quite hard to find references to a particular callback method.
#31 by Georgios Adamopoulos on January 25th, 2021 ·
Thanks you for this excellent analysis!
While reading this, and having in mind just how convenient it is from a designer’s perspective to see a nice visual list of the operations that an object should trigger (as is the case with UnityEvent and friends), I wondered if it makes sense to strive for the best of both worlds, by using UnityEvents purely as a graphical convenience, and using Reflection behind the scenes to convert the UnityEvent’s Persistent Listeners to delegates once, in the OnEnable or Awake method.
I am about to try this, but I would love to hear your opinion !
#32 by jackson on January 25th, 2021 ·
There may be a high cost in terms, especially in terms of GC allocations, if you have a lot of events. Another hybrid approach would be to build your own Inspector UI backed by C# events instead of UnityEvent. Feel free to experiment, especially within the constraints of your particular project.
#33 by Georgios Adamopoulos on January 31st, 2021 ·
I think Thor Brigsted did exactly that !
https://github.com/Siccity/SerializableCallback
#34 by Kevin on February 16th, 2021 ·
The performance difference doesn’t necessarily mean anything if you have to perform millions of operations to even discern a difference in speed.
For example, if it takes 206ms to invoke a UnityEvent 10 million times, that means each invocation takes ~.0000206 milliseconds. In other words, we would have to invoke the event ~48,544 times in one frame to waste 1ms of CPU time.
Unless you’re invoking tens of thousands of Unity events _every frame_ (in which case you likely have bigger problems!), the difference in performance cost doesn’t matter.
#35 by jackson on February 20th, 2021 ·
I agree that the difference on this CPU with 0 arguments and 1 listener is negligible for most games. However, if you take the 2 arguments and 5 listeners number then 8425 invocations are needed to eat up 1ms. At the time this article was written five years ago, the Samsung Galaxy S6 was a typical flagship Android device with game developers supporting Android devices like the Samsung Galaxy S4. Just to approximate, the CPU used in the article’s test has a single-core Geekbench score of 689 while the Samsung Galaxy S4 has a 151. Applying that ~4.5x difference gives us 1846 invocations per 1ms. That’s still a lot of event invoking, but it may represent a few percent of a frame’s CPU time for a sufficiently complex game that relies heavily on event dispatching. I do agree though that for most games, especially five years later, the difference is unimportant.
#36 by Lorenzo Tesler-Mabe on May 25th, 2021 ·
Hey! Just want to clarify/ask a few things in 2021 terms.
I am currently firing off more Unity Events than I’d like as part of the main logic of my game. I am thinking about switching over the most frequent offenders to System.Actions or c# events.
Is this still worth doing in 2021? Sure, I’m often calling multiple Unity Events in the span of a few seconds, but that is still nill in terms of cpu impact. I’m the most curious about GC alloc. Is it still true that calling Unity events will only generate garbage the first time they are called? In any case, switching to delegates for commonly fired logic will save me from all GC alloc whatsoever (aside from creation), correct?
Thanks so much, and apologies for the lack of self testing… I am not proficient with the profiler.
#37 by jackson on June 1st, 2021 ·
Hey! I’d recommend running the tests from the article on your target device with your specific version of Unity. That way you’ll get the most-relevant possible data. You might even want to tweak some of the constants I used to numbers that make sense for your project. It’s a relatively straightforward (instructions are mostly in the article) and quick process that’s a good skill to learn as you can repeat the process with a great many tests to gain a lot of knowledge that’s useful in decision-making. Feel free to post your results here!
#38 by Bayu on September 4th, 2021 ·
Hi Jackson,
Thanks for sharing your tests. What do you think about Ryan Hipple’s Scriptable Object event architecture? Unity advocates used it for the first Unity Open Project, but I’m concerned about the garbage memory generated by the Unity Event in the architecture.
#39 by Ivan on February 12th, 2023 ·
Hi Jackson,
Thank you for the article, it was quite insightful! I’ve decided to re-run the tests on my MacBook in 2023 to see if it still holds, and it looks like it does :) It looks like the difference is especially evident (UnityEvents are 8-9 times slower) when there is just one listener. Here are my results in the following setup:
2.3 Ghz 8-core i9
macOS Monterey 12.4
Unity 2021.3.10f1, Mac OS X Standalone, intel 64-bit only, non-development
640×480, Fastest, Windowed