.class public auto ansi beforefieldinit TestScript
extends [UnityEngine]UnityEngine.MonoBehaviour
// Fields
.field private static class [System.Core]System.Action '<>f__mg$cache0'
.custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
01 00 00 00
.field private static class [System.Core]System.Action '<>f__am$cache0'
.custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
01 00 00 00
.field private static class [System.Core]System.Action '<>f__am$cache1'
.custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
01 00 00 00
.method private hidebysig
instance void TestInstanceFunction () cil managed
// Method begins at RVA 0x209d
// Code size 20 (0x14)
.maxstack 8
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldarg.0
IL_0003: ldftn instance void TestScript::InstanceFunction()
IL_0009: newobj instance void [System.Core]System.Action::.ctor(object, native int)
IL_000e: call instance void TestScript::TakeDelegate(class [System.Core]System.Action)
IL_0013: ret
} // end of method TestScript::TestInstanceFunction
.method private hidebysig
instance void TestStaticFunction () cil managed
// Method begins at RVA 0x20b2
// Code size 37 (0x25)
.maxstack 8
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldsfld class [System.Core]System.Action TestScript::'<>f__mg$cache0'
IL_0007: brtrue.s IL_001a
IL_0009: ldnull
IL_000a: ldftn void TestScript::StaticFunction()
IL_0010: newobj instance void [System.Core]System.Action::.ctor(object, native int)
IL_0015: stsfld class [System.Core]System.Action TestScript::'<>f__mg$cache0'
IL_001a: ldsfld class [System.Core]System.Action TestScript::'<>f__mg$cache0'
IL_001f: call instance void TestScript::TakeDelegate(class [System.Core]System.Action)
IL_0024: ret
} // end of method TestScript::TestStaticFunction
.method private hidebysig
instance void TestLambda () cil managed
// Method begins at RVA 0x20d8
// Code size 37 (0x25)
.maxstack 8
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldsfld class [System.Core]System.Action TestScript::'<>f__am$cache0'
IL_0007: brtrue.s IL_001a
IL_0009: ldnull
IL_000a: ldftn void TestScript::'<TestLambda>m__0'()
IL_0010: newobj instance void [System.Core]System.Action::.ctor(object, native int)
IL_0015: stsfld class [System.Core]System.Action TestScript::'<>f__am$cache0'
IL_001a: ldsfld class [System.Core]System.Action TestScript::'<>f__am$cache0'
IL_001f: call instance void TestScript::TakeDelegate(class [System.Core]System.Action)
IL_0024: ret
} // end of method TestScript::TestLambda
.method private hidebysig
instance void TestAnonymousMethod () cil managed
// Method begins at RVA 0x20fe
// Code size 37 (0x25)
.maxstack 8
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldsfld class [System.Core]System.Action TestScript::'<>f__am$cache1'
IL_0007: brtrue.s IL_001a
IL_0009: ldnull
IL_000a: ldftn void TestScript::'<TestAnonymousMethod>m__1'()
IL_0010: newobj instance void [System.Core]System.Action::.ctor(object, native int)
IL_0015: stsfld class [System.Core]System.Action TestScript::'<>f__am$cache1'
IL_001a: ldsfld class [System.Core]System.Action TestScript::'<>f__am$cache1'
IL_001f: call instance void TestScript::TakeDelegate(class [System.Core]System.Action)
IL_0024: ret
} // end of method TestScript::TestAnonymousMethod
At the top you can see that the compiler generated not one, but three static Action
fields. In TestInstanceFunction
you can see the new
/newobj
for Action
. The other three types are all implemented the same way and they all do a null
check and reuse their static, cached field.
At this point you’ve seen that passing an instance function for a delegate parameter always creates garbage and passing any other type of function makes the compiler insert an if
every time. Both of these are undesirable. You really want to avoid the garbage creation and kind of want to avoid the slow branching of if
.
Thankfully, you can take manual control and make your own cached Action
fields. Then you can set them up when it’s convenient for you and avoid the if
check every time you use them. Even better, you can cache an Action
for instance functions and avoid the garbage creation!
Here’s a modified version of the test script to test out that idea:
using System;
using UnityEngine;
public class TestScript : MonoBehaviour
Action instanceFunctionDelegate;
static Action staticFunctionDelegate;
static Action lambdaDelegate;
static Action anonymousMethodDelegate;
void Start()
CreateDelegates();
TestFirst();
TestSecond();
void CreateDelegates()
CreateInstanceFunctionDelegate();
CreateStaticFunctionDelegate();
CreateLambdaDelegate();
CreateAnonymousMethodDelegate();
void CreateInstanceFunctionDelegate()
instanceFunctionDelegate = InstanceFunction;
void CreateStaticFunctionDelegate()
staticFunctionDelegate = StaticFunction;
void CreateLambdaDelegate()
lambdaDelegate = () => {};
void CreateAnonymousMethodDelegate()
anonymousMethodDelegate = delegate(){};
void TestFirst()
TestInstanceFunction();
TestStaticFunction();
TestLambda();
TestAnonymousMethod();
void TestSecond()
TestInstanceFunction();
TestStaticFunction();
TestLambda();
TestAnonymousMethod();
void TestInstanceFunction()
TakeDelegate(instanceFunctionDelegate);
void TestStaticFunction()
TakeDelegate(staticFunctionDelegate);
void TestLambda()
TakeDelegate(lambdaDelegate);
void TestAnonymousMethod()
TakeDelegate(anonymousMethodDelegate);
void TakeDelegate(Action del)
void InstanceFunction(){}
static void StaticFunction(){}
And here’s how it looks in the profiler:
You can see that the garbage is created when setting up the cached Action
fields but never when actually using them later. The first goal is accomplished: passing instance methods no longer creates garbage. But what about the if
check? To confirm that it’s gone, check out the IL now:
.class public auto ansi beforefieldinit TestScript
extends [UnityEngine]UnityEngine.MonoBehaviour
// Fields
.field private class [System.Core]System.Action instanceFunctionDelegate
.field private static class [System.Core]System.Action staticFunctionDelegate
.field private static class [System.Core]System.Action lambdaDelegate
.field private static class [System.Core]System.Action anonymousMethodDelegate
.field private static class [System.Core]System.Action '<>f__mg$cache0'
.custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
01 00 00 00
.field private static class [System.Core]System.Action '<>f__am$cache0'
.custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
01 00 00 00
.field private static class [System.Core]System.Action '<>f__am$cache1'
.custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
01 00 00 00
.method private hidebysig
instance void TestInstanceFunction () cil managed
// Method begins at RVA 0x2142
// Code size 14 (0xe)
.maxstack 8
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldarg.0
IL_0003: ldfld class [System.Core]System.Action TestScript::instanceFunctionDelegate
IL_0008: call instance void TestScript::TakeDelegate(class [System.Core]System.Action)
IL_000d: ret
} // end of method TestScript::TestInstanceFunction
.method private hidebysig
instance void TestStaticFunction () cil managed
// Method begins at RVA 0x2151
// Code size 13 (0xd)
.maxstack 8
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldsfld class [System.Core]System.Action TestScript::staticFunctionDelegate
IL_0007: call instance void TestScript::TakeDelegate(class [System.Core]System.Action)
IL_000c: ret
} // end of method TestScript::TestStaticFunction
.method private hidebysig
instance void TestLambda () cil managed
// Method begins at RVA 0x215f
// Code size 13 (0xd)
.maxstack 8
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldsfld class [System.Core]System.Action TestScript::lambdaDelegate
IL_0007: call instance void TestScript::TakeDelegate(class [System.Core]System.Action)
IL_000c: ret
} // end of method TestScript::TestLambda
.method private hidebysig
instance void TestAnonymousMethod () cil managed
// Method begins at RVA 0x216d
// Code size 13 (0xd)
.maxstack 8
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldsfld class [System.Core]System.Action TestScript::anonymousMethodDelegate
IL_0007: call instance void TestScript::TakeDelegate(class [System.Core]System.Action)
IL_000c: ret
} // end of method TestScript::TestAnonymousMethod
Now all four of these functions is implemented the same. They simply pass their static field to TakeDelegate
without any null
check. Unfortunately, the compiler leaves in its own cache fields so there’s duplication now. So you have a tradeoff: do you want duplicate fields or duplicate null
checks?
With this strategy you can get control over the garbage that’s created when you use delegates. It’s really easy to cache the delegates as fields and the GC win can be quite large. So keep this in mind next time you’re passing a callback!
Let me know in the comments how you deal with garbage creation and delegates.
Comments
That’s allowed in C#. The class created for the delegate will have fields matching the local variables it accesses. When the class is instantiated, the local variables are copied to those fields. All of the local variable accesses from inside the delegate are then rewritten to be accesses of those fields. For more on closures, check out this more recent article.
Thanks for your reply. Here is a little more information about my question. Let’s say I used to do this :
private float _myVar3;
private void TestLambda()
float myVar;
// Do something with myVar here
this.TakeDelegate(delegate
bool myVar2 = _myVar3 * myVar;
});
Now that I read your article, I realize why I get cpu spikes at the first frame due to garbage creation and I really want to avoid it! So I add a static cached Action field for my delegate :
static Action _lambdaDelegate;
void CreateLambdaDelegate()
_lambdaDelegate = () =>
// How can I get myVar here?
private void TestLambda()
float myVar;
// Do something with myVar here
// How can I pass myVar to lambdaDelegate without leaving this current scope?
TakeDelegate(_lambdaDelegate);
I hope you understand my problem better. I’d like to not leave my current scope (within the TestLambda function) and be able to pass myVar to my delegate. However I’m not aware of passing parameters to delegate. Maybe there is another solution?
Thanks a lot
private void TestLambda()
// How can I pass myVar to lambdaDelegate without leaving this current scope?
TakeDelegate(_lambdaDelegate);
private void TakeDelegate(Action del)
del();
You can also take parameters in lambdas:
static Action<float> _lambdaDelegate;
void CreateLambdaDelegate()
_lambdaDelegate = (float myVar) =>
// Do something with myVar here
private void TestLambda()
// How can I pass myVar to lambdaDelegate without leaving this current scope?
TakeDelegate(_lambdaDelegate);
private void TakeDelegate(Action<float> del)
del(3.14f);
Hi, thank you for this article. It’s very informative. :)
I have one question: is this predicate also garbage safe?
private System.Predicate<ITag> CompareTagsDelegate => x => x.Tag == _tagToFind;
I tried your example and when passing a static method to a method that takes a delegate, eg:
TakeDelegate(StaticFunction);
Unity’s profiler shows that there is a GC allocation. Has there been a change in how the compiler works in this regard in recent versions of Unity? I’m on 2020.3.25f1 (LTS).
I’m seeing the same thing on 2021.3.0 and 2019.4.18. Not sure how far back it goes.
Even stranger, if you wrap the static function call in a lambda, it _will_ statically cache it and only allocate the first time (similar to above).
TakeDelegate(StaticFunction); // allocates every time
TakeDelegate(() => StaticFunction()); // allocates once
I’m annoyed the compiler doesn’t optimize that but maybe there’s a reason I’m not seeing (I’m no expert). I would love some more insight on this.
By the way, if anyone’s interested in how C# 9.0’s static lambdas affect all this, from what I can tell all it’s doing is preventing closures. Meaning, it’s exactly the same as using a lambda without a closure:
void TakeDelegate(Action act) { }
TakeDelegate(InstanceFunction); // allocates every time
TakeDelegate(() => InstanceFunction()); // allocates every time
// but if you can do something like:
void TakeDelegate(TestScript val, Action<TestScript> act) { }
TakeDelegate(this, (t) => t.InstanceFunction()); // allocates once
TakeDelegate(this, static (t) => t.InstanceFunction()); // allocates once
Precaching the delegates can be nice, but personally, I’ll just be doing static lambdas most of the time now. Worrying about if-else branching seems extreme even for me as a chronic premature optimizer.