添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

Delegates and Garbage Creation

February 27, 2017 Tags: , , , ,

Two facts are at odds in Unity programming. First, delegates like Action , Func , and EventHandler are extremely common with or without events. Second, the garbage collector is a huge source of CPU spikes and memory fragmentation in our games. Why are these facts at odds? Because code that uses delegates is almost always written in a way that creates garbage. It’s an extremely easy trap to fall into, but this article will show you how to get out of it!

Consider a very simple function:

void TakeDelegate(Action del)

You’ve probably seen a thousand functions like this. Array.Find takes a delegate and so does List.RemoveAll. You’ve probably passed a thousand “callback” variables, each some kind of delegate. The same goes for every event keyword you’ve ever seen: they’re just thinly veiled delegates.

So delegates are everywhere, but who cares? Well, if you care about preventing CPU spikes and not running out of memory due to fragmentation then you should care. That’s because the simplest way of calling that TakeDelegate function involves creating 104 bytes of garbage that the garbage collector (GC) will someday collect. All in one frame. On the main thread. And fragment your memory.

Here’s the simple way that almost all code calls TakeDelegate:

void MyFunction()
TakeDelegate(MyFunction);

Did you spot the garbage creation? It’s really hard to see because the garbage creation is inserted into our code by the compiler! The compiler rewrites our code to leave out a missing step:

TakeDelegate(new Action(MyFunction));

And there you see the dreaded new keyword. Every one of these calls to TakeDelegate creates a new Action which is 104 bytes of garbage. What if you call it every frame in your 30 FPS game? That’s about 3 KB of garbage per second for just that one function call!

To prove this, here’s a tiny test script:

using System;
using UnityEngine;
public class TestScript : MonoBehaviour
	void Start()
		TestFirst();
		TestSecond();
	void TestFirst()
		TestInstanceFunction();
		TestStaticFunction();
		TestLambda();
		TestAnonymousMethod();
	void TestSecond()
		TestInstanceFunction();
		TestStaticFunction();
		TestLambda();
		TestAnonymousMethod();
	void TestInstanceFunction()
		TakeDelegate(InstanceFunction);
	void TestStaticFunction()
		TakeDelegate(StaticFunction);
	void TestLambda()
		TakeDelegate(() => {});
	void TestAnonymousMethod()
		TakeDelegate(delegate(){});
	void TakeDelegate(Action del)
	void InstanceFunction(){}
	static void StaticFunction(){}

If you run this and look at the Unity profiler in “deep” profiling mode you’ll see the first frame has this:

All four types of functions—instance functions, static functions, lambdas, and anonymous methods—allocate 104 bytes the first time you call TakeDelegateTakeDelegate, but the other types don’t. Why?

To find out, you have to decompile Library/ScriptAssemblies/Assembly-CSharp.dll and look at what else the compiler is generating on your behalf. Here’s what ILSpy will show you in C# mode:

using System;
using System.Runtime.CompilerServices;
using UnityEngine;
public class TestScript : MonoBehaviour
	[CompilerGenerated]
	private static Action <>f__mg$cache0;
	private void Start()
		this.TestFirst();
		this.TestSecond();
	private void TestFirst()
		this.TestInstanceFunction();
		this.TestStaticFunction();
		this.TestLambda();
		this.TestAnonymousMethod();
	private void TestSecond()
		this.TestInstanceFunction();
		this.TestStaticFunction();
		this.TestLambda();
		this.TestAnonymousMethod();
	private void TestInstanceFunction()
		this.TakeDelegate(new Action(this.InstanceFunction));
	private void TestStaticFunction()
		if (TestScript.<>f__mg$cache0 == null)
			TestScript.<>f__mg$cache0 = new Action(TestScript.StaticFunction);
		this.TakeDelegate(TestScript.<>f__mg$cache0);
	private void TestLambda()
		this.TakeDelegate(delegate
		});
	private void TestAnonymousMethod()
		this.TakeDelegate(delegate
		});
	private void TakeDelegate(Action del)
	private void InstanceFunction()
	private static void StaticFunction()

For the static function, ILSpy generated a static Action variable and adds a null check every time you try to call TakeDelegate. The effect is that only one delegate is ever created for the static function and that explains why the second time you call TakeDelegate there’s no garbage created.

For the lambda and anonymous method you just see delegate{} which isn’t very helpful. You have to switch to IL mode to see the actual IL instead of a C# representation of it. Here’s a snippet of the most interesting parts:

.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.