Why was automatic static delegate caching turned off in Unity 2019.2?

Starting with Unity version 2019.2.0f1, Unity stopped generating IL code capable of caching static delegates. Prior to that version, you could run the following code without generating any garbage:

{
public void Update()
{
this.DoCallback(StaticCallback);
}

private void DoCallback(Action action)
{
action();
}

private static void StaticCallback()
{
Debug.Log("Static callback");
}
}```

However, if you run the same code on any newer verison of Unity, the Update() method allocates a new delegate every call, creating unnecessary garbage.

Here is how Unity used to generate IL for the Update function before 2019.2:

```instance void Update () cil managed
{
.maxstack 8

// {
IL_0000: nop
// DoCallback(StaticCallback);
IL_0001: ldarg.0
IL_0002: ldsfld class [System.Core]System.Action GarbageTest::'<>f__mg$cache0'
IL_0007: brtrue.s IL_001a

IL_0009: ldnull
IL_000a: ldftn void GarbageTest::StaticCallback()
IL_0010: newobj instance void [System.Core]System.Action::.ctor(object, native int)
IL_0015: stsfld class [System.Core]System.Action GarbageTest::'<>f__mg$cache0'

IL_001a: ldsfld class [System.Core]System.Action GarbageTest::'<>f__mg$cache0'
// }
IL_001f: call instance void GarbageTest::smile:oCallback(class [System.Core]System.Action)
IL_0024: ret
}```

And here is how Unity now generates IL:

```instance void Update () cil managed
{
.maxstack 8

// {
IL_0000: nop
// DoCallback(StaticCallback);
IL_0001: ldarg.0
IL_0002: ldnull
IL_0003: ldftn void GarbageTest::StaticCallback()
IL_0009: newobj instance void [netstandard]System.Action::.ctor(object, native int)
IL_000e: call instance void GarbageTest::smile:oCallback(class [netstandard]System.Action)
// }
IL_0013: nop
IL_0014: ret
}```

As you can see, there is no longer any attempt to cache the static delegate, which means garbage is generated by every call to Update().

Why was delegate caching turned off in 2019.2? And can we get it turned back on?

Just an assumption because it seems related. Perhaps this change was made to allow for “disabling domain reload”?

Static fields would then not get cleared on domain reload and you need to take care of resetting statics yourself. The benefit of course is encouraging less use of statics (rarely required anyway) in order to allow for instantaneous “enter playmode”. Well worth it!

This is a plausible theory, although if that was their intent, their approach did not fully address the issue, because Unity still does static delegate caching if you use a lambda expression instead of a named function.

This all came up because we noticed that Unity’s latest version of their input system was generating garbage every frame . The culprit? A static delegate call inside an Update method.

2 Likes

Method group static methods wasn’t cached for a very long time.
They fixed that thing just a couple of years ago.
Call(() => MyStaticMethod()) was always cached as far as I know.
You can open SharpLab and check different version, it wasn’t cached even in 2021, and according to info I found it was fixed in C# 11/.NET 7, and Unity uses Mono anyway, so just use lambda syntax.

It’s the version of the C# compiler (csc, not mono’s mcs) that matters since the behavior is defined at the IL level. 2019.2.0f1 ships with a forked Roslyn (9d34608e) from late 2018, and 6000.0.6f1 ships with one from late 2022 (41a5af9d), right around the time when .NET 7 was getting ready to go GA. Neither version does static delegate caching with the provided sample code. Given that 2019.2 dropped .NET Framework 3.5 scripting and the mono C# compiler along with it perhaps mono’s compiler had been doing static delegate caching (have not personally verified this) while Roslyn didn’t for a while.

2 Likes