Do Events and Delegates Create Garbage?
Unity’s garbage collector can be disastrous to our games’ framrates when it runs so we’d best not incur its wrath. We’ve seen that foreach
loops usually create garbage, so the natural followup question is “what other language features create garbage?” Events and delegates are extremely handy features of C#. They serve as the function pointers and Function
objects of the language. They replace signals and slots and allow for flexible callbacks. But a lot of what they do is behind the scenes. Are they creating garbage back there? Today’s article puts them to the test to see if creating and calling delegates and events creates any garbage. Read on to find out!
Delegates come in a few forms. First, you declare a delegate type like this:
// Takes no parameters, returns void delegate void MyDelegate();
Then you can make one with a lambda:
MyDelegate del = () => {};
Or an anonymous delegate function:
MyDelegate del = delegate() {};
Or use an existing function:
void Foo() {} MyDelegate del = Foo;
Or use one of the many overloads of Delegate.CreateDelegate
:
// This overload takes a delegate type, instance, and function name // It returns a Delegate, so you have to cast to your particular type of delegate MyDelegate del = (MyDelegate)Delegate.CreateDelegate(typeof(MyDelegate), this, "Foo");
In any case, you get an instance of your delegate type that you can call like a function:
del();
You can also add more functions, lambdas, anonymous delegate functions, and other delegates on to your delegate instance:
// Add a lambda del += () => {};
Then when you call the delegate, it calls the original function plus all the ones you’ve added on:
MyDelegate del = () => { Debug.Log("original"); }; del += () => { Debug.Log("added"); }; del(); // prints "original" then "added"
Events are very similar to delegates. You declare one using a delegate type and the event
keyword:
event MyDelegate Event;
Then you can add on to it, just like a delegate:
Event += () => {};
One crucial difference is that the event can only be dispatched and set from the class that declares it:
class MyClass { public event MyDelegate Event; } MyClass mc = new MyClass(); mc.Event(); // compiler error, tried to call from outside of MyClass mc.Event = () => {}; // compiler error, tried to set from outside of MyClass
You can also customize how the event has delegates added and removed:
class MyClass { private int numListeners; private MyDelegate _event; public event MyDelegate Event { add { _event += value; numListeners++; } remove { _event -= value; numListeners--; } } } MyClass mc = new MyClass(); // numListeners == 0 mc.Event += () => {}; // numListeners == 1 mc.Event -= () => {}; // numListeners == 0
That’s delegates and events in a nutshell! With that in mind, let’s set up a test script to create, add to, and call delegates and events. We’ll then run the script in Unity and use the profiler to see which actions caused garbage to be created/allocated. Because Unity displays this information on a per-function basis, we have to create a lot of duplicate functions to make sure we don’t lump together the garbage from multiple actions. Here’s the script I came up with:
using System; using UnityEngine; delegate void MyDelegate(); class EmptyClass { } class ClassWithEvent { public event MyDelegate Event; public void Dispatch() { Event(); } } class TestScript : MonoBehaviour { void Start() { var lambda = CreateLambda(); var anonymousDelegate = CreateAnonymousDelegate(); var functionAsDelegate = ReturnFunctionAsDelegate(); var delegateCreateDelegate = DelegateCreateDelegate(); CallLambda(lambda); CallAnonymousDelegate(anonymousDelegate); CallFunctionAsDelegate(functionAsDelegate); CallDelegateCreateDelegate(delegateCreateDelegate); InstantiateEmptyClass(); var classWithEvent = InstantiateClassWithEvent(); AddFirstDelegateToEvent(classWithEvent, lambda); AddSecondDelegateToEvent(classWithEvent, lambda); AddThirdDelegateToEvent(classWithEvent, lambda); AddFourthDelegateToEvent(classWithEvent, lambda); AddFifthDelegateToEvent(classWithEvent, lambda); AddSixthDelegateToEvent(classWithEvent, lambda); AddSeventhDelegateToEvent(classWithEvent, lambda); AddEighthDelegateToEvent(classWithEvent, lambda); AddNinthDelegateToEvent(classWithEvent, lambda); AddTenthDelegateToEvent(classWithEvent, lambda); DispatchEvent(classWithEvent); AddFirstDelegateToDelegate(lambda, anonymousDelegate); AddSecondDelegateToDelegate(lambda, anonymousDelegate); AddThirdDelegateToDelegate(lambda, anonymousDelegate); AddFourthDelegateToDelegate(lambda, anonymousDelegate); AddFifthDelegateToDelegate(lambda, anonymousDelegate); AddSixthDelegateToDelegate(lambda, anonymousDelegate); AddSeventhDelegateToDelegate(lambda, anonymousDelegate); AddEighthDelegateToDelegate(lambda, anonymousDelegate); AddNinthDelegateToDelegate(lambda, anonymousDelegate); AddTenthDelegateToDelegate(lambda, anonymousDelegate); } MyDelegate CreateLambda() { return () => {}; } MyDelegate CreateAnonymousDelegate() { return delegate() {}; } MyDelegate ReturnFunctionAsDelegate() { return Foo; } MyDelegate DelegateCreateDelegate() { return (MyDelegate)Delegate.CreateDelegate(typeof(MyDelegate), this, "Foo"); } void Foo() {} void CallLambda(MyDelegate lambda) { lambda(); } void CallAnonymousDelegate(MyDelegate anonymousDelegate) { anonymousDelegate(); } void CallFunctionAsDelegate(MyDelegate functionAsDelegate) { functionAsDelegate(); } void CallDelegateCreateDelegate(MyDelegate delegateCreateDelegate) { delegateCreateDelegate(); } void InstantiateEmptyClass() { new EmptyClass(); } ClassWithEvent InstantiateClassWithEvent() { return new ClassWithEvent(); } void AddFirstDelegateToEvent(ClassWithEvent classWithEvent, MyDelegate del) { classWithEvent.Event += del; } void AddSecondDelegateToEvent(ClassWithEvent classWithEvent, MyDelegate del) { classWithEvent.Event += del; } void AddThirdDelegateToEvent(ClassWithEvent classWithEvent, MyDelegate del) { classWithEvent.Event += del; } void AddFourthDelegateToEvent(ClassWithEvent classWithEvent, MyDelegate del) { classWithEvent.Event += del; } void AddFifthDelegateToEvent(ClassWithEvent classWithEvent, MyDelegate del) { classWithEvent.Event += del; } void AddSixthDelegateToEvent(ClassWithEvent classWithEvent, MyDelegate del) { classWithEvent.Event += del; } void AddSeventhDelegateToEvent(ClassWithEvent classWithEvent, MyDelegate del) { classWithEvent.Event += del; } void AddEighthDelegateToEvent(ClassWithEvent classWithEvent, MyDelegate del) { classWithEvent.Event += del; } void AddNinthDelegateToEvent(ClassWithEvent classWithEvent, MyDelegate del) { classWithEvent.Event += del; } void AddTenthDelegateToEvent(ClassWithEvent classWithEvent, MyDelegate del) { classWithEvent.Event += del; } void DispatchEvent(ClassWithEvent classWithEvent) { classWithEvent.Dispatch(); } void AddFirstDelegateToDelegate(MyDelegate del, MyDelegate toAdd) { del += toAdd; } void AddSecondDelegateToDelegate(MyDelegate del, MyDelegate toAdd) { del += toAdd; } void AddThirdDelegateToDelegate(MyDelegate del, MyDelegate toAdd) { del += toAdd; } void AddFourthDelegateToDelegate(MyDelegate del, MyDelegate toAdd) { del += toAdd; } void AddFifthDelegateToDelegate(MyDelegate del, MyDelegate toAdd) { del += toAdd; } void AddSixthDelegateToDelegate(MyDelegate del, MyDelegate toAdd) { del += toAdd; } void AddSeventhDelegateToDelegate(MyDelegate del, MyDelegate toAdd) { del += toAdd; } void AddEighthDelegateToDelegate(MyDelegate del, MyDelegate toAdd) { del += toAdd; } void AddNinthDelegateToDelegate(MyDelegate del, MyDelegate toAdd) { del += toAdd; } void AddTenthDelegateToDelegate(MyDelegate del, MyDelegate toAdd) { del += toAdd; } }
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 check for garbage collector allocations like this:
- Open the
Profiler
editor pane - Select the
Deep Profile
button - Click the
Clear
button - Click the
Play
button in the main editor window - Click the
Play
button again shortly after the app starts - In the
Profiler
editor pane, select theCPU Usage
section - Click in the timeline on the first frame or anywhere to the left of it
- Find
TestScript.Start()
in the bottom left section and click its triangle - Click the triangles for all the test functions under
TestScript.Start()
- Look at the values in the
GC Alloc
column for each function call
I followed these steps on Mac OS X 10.11 with Unity 5.2.2f1 and found the following results:
Action | Garbage Created |
---|---|
Create Lambda | 104 bytes |
Create Anonymous Delegate | 104 bytes |
Return Function as Delegate | 104 bytes |
Delegate.CreateDelegate |
0.5 KB |
Call Lambda | 0 bytes |
Call Anonymous Delegate | 0 bytes |
Call Function as Delegate | 0 bytes |
Call Delegate.CreateDelegate result |
0 bytes |
Add First Delegate to Delegate | 0 bytes |
Add Second Delegate to Delegate | 208 bytes |
Add Third Delegate to Delegate | 208 bytes |
Add Fourth Delegate to Delegate | 208 bytes |
Add Fifth Delegate to Delegate | 208 bytes |
Add Sixth Delegate to Delegate | 208 bytes |
Add Seventh Delegate to Delegate | 208 bytes |
Add Eighth Delegate to Delegate | 208 bytes |
Add Ninth Delegate to Delegate | 208 bytes |
Add Tenth Delegate to Delegate | 208 bytes |
Instantiate Empty Class | 16 bytes |
Instantiate Class with Event | 24 bytes |
Dispatch Event | 0 bytes |
Add First Delegate to Event | 0 bytes |
Add Second Delegate to Event | 208 bytes |
Add Third Delegate to Event | 312 bytes |
Add Fourth Delegate to Event | 416 bytes |
Add Fifth Delegate to Event | 0.5 KB |
Add Sixth Delegate to Event | 0.6 KB |
Add Seventh Delegate to Event | 0.7 KB |
Add Eighth Delegate to Event | 0.8 KB |
Add Ninth Delegate to Event | 0.9 KB |
Add Tenth Delegate to Event | 1.0 KB |
Creating a delegate seems to allocate 104 bytes of garbage. That’s true for lambdas, anonymous delegate functions, and even returning a regular function as a delegate. The Delegate.CreateDelegate
function, however, creates 0.5 KB of garbage. This makes sense since it’s clearly using reflection to get the function by a string name and create the delegate from there.
While it’s a shame that creating the delegate creates garbage too, calling the delegate doesn’t create any. It doesn’t seem to matter how the delegate was created, even if you use Delegate.CreateDelegate
.
Adding the first delegate to an existing delegate (with +=
) creates no garbage. Once you add another delegate, it creates 208 bytes of garbage. This holds for each delegate you add on, all the way up to where I stopped at 10.
Event creation is a bit tricky to test. Events are, by definition, part of a class. So you need to instantiate a class in order to instantiate the event. The test script instantiates an empty class and a class that just has an event. The empty class created 16 bytes of garbage and the class with just an event created 24. That’s an extra 8 bytes for the event field.
Dispatching the event, like dispatching a delegate, creates no garbage.
Adding delegates to an event is the most painful part. The first one creates no garbage, but each subsequent one creates 104 bytes more. This means that the second is 208, the third is 312, the fourth is 416, then 0.5 KB, 0.6 KB, 0.7 KB, 0.8 KB, 0.9 KB, and 1.0 KB for the tenth.
In summary, garbage will be created when you use either delegates or events. Importantly, the garbage is created as part of the setup process: creating and adding. Dispatching and calling never creates any garbage though, so you can use them every frame without any consequences on the GC front.
Feel free to share your thoughts on delegates, events, and garbage collection in the comments section below!
#1 by Laurens Holst on March 10th, 2017 ·
I think delegates only create garbage if they use a closure.
#2 by jackson on March 10th, 2017 ·
If by “closure” you mean that the delegate references local variables then the delegates in this articles wouldn’t count, but they still create garbage.
#3 by Michael B on November 26th, 2017 ·
Great article. I’ve been exploring UnityEvents and it appears that invoking UnityEvents triggers a rebuild of the InvokableCallersList, which generates GC every time.
Modified TestScript:
#4 by Michael B on November 26th, 2017 ·
A bit more testing. After multicasting and object instantiations, calling all events, delegates, and UnityEvents in Update shows 0 GC allocation in a dev build connected to the profiler.
I have tested serialized UnityEvents to which objects are connected via the Inspector, and most of them DO create GC pressure when similarly profiled. Not sure what explains this, but it limits the viability of serialized UnityEvents (In Unity 5.5 at least).
#5 by jackson on November 26th, 2017 ·
Thanks for posting your findings. It’s interesting to hear that serialized UnityEvents are allocating but non-serializing UnityEvents aren’t. I wonder if that’s been improved in the three releases since Unity 5.5.
#6 by Michael B on November 26th, 2017 ·
…and more testing. Serialized UnityEvents with objects/callbacks connected via the inspector generate GC, while listeners dynamically added to the SAME serialzed events in code do not.
There must be something in the serialized connection to other scene objects which disturbs the heap, thus triggering GC. This is a significant failing, as UnityEvents are by far the easiest way to decouple code. The necessity of dynamically adding listeners creates unnecessary dependencies :/
#7 by Michael B on November 26th, 2017 ·
Testing in 5.6 and 2017.1 show the same GC problem.
For simple value/object sync, scriptable objects are an option, but again, they introduce dependencies.
#8 by jackson on November 26th, 2017 ·
That’s a real shame. This probably warrants some deeper digging, such as decompiling UnityEngine.dll to see what’s going on in UnityEvent. I’ve made a note to do this, but it’ll probably take me a while to get around to it. Thanks for reporting this info!
#9 by Luke on April 4th, 2018 ·
I’ve had a lot of problems with delegate and event allocation on projects that use them heavily, so I’ve written a system called Relay that mitigates these issues. It’s fast and lean, GC-friendly, adds additional functionality over event, and has a debug mode to track down lapsed listeners. Thought you might be interested!
https://github.com/SixWays/Relay
#10 by jackson on April 4th, 2018 ·
This looks like it could be really handy for anyone using a lot of events. I’ve only given it a quick look so far, but it looks great from what I’ve seen!
#11 by Luke on April 8th, 2018 ·
Thanks :) If you end up trying it out at any point, I’d love to hear any feedback, or better yet pull requests!
#12 by Steve on May 30th, 2023 ·
Still exists in 2021.3 in Editor & Builds.
But I do know one weird trick.
Let’s say you have a NamedStaticMethod() that you’re passing into another method that accepts an Action. It will allocate 128 bytes.
BUT if you wrap it in a lambda it will allocate *** 0 bytes ***
() => NamedStaticMethod();
No idea why that works at all.