C
C#8mo ago
br4kejet

✅ Compiler creates closure when it's not necessary?

I have this function here:
public static void Run<T>(Action<T> action, T parameter) => action(parameter);
public static void Run<T>(Action<T> action, T parameter) => action(parameter);
And then I'm using it like this:
MyObject obj = null;
Run(t => t.Item1.DoThing(t.Item2), (this, obj));
MyObject obj = null;
Run(t => t.Item1.DoThing(t.Item2), (this, obj));
But I have the "Heap Allocation Viewer" plugin installed in Rider, and it says it allocates a closure for the variable t? Even when I run my app, and debug the Run function, it seems to create a hidden class. Why does it do this, even though the Action isn't accessing any variables in the outer scope?
22 Replies
WEIRD FLEX
WEIRD FLEX8mo ago
apart from this?
br4kejet
br4kejet8mo ago
EVen if I don't use a tuple as the parameter, and just pass "this", it still says closure allocation
jcotton42
jcotton428mo ago
that's being passed as a parameter, it's not used in the lambda
WEIRD FLEX
WEIRD FLEX8mo ago
should the compiler know?
jcotton42
jcotton428mo ago
erm, yes? the compiler does capture analysis to know what to include in the closure
br4kejet
br4kejet8mo ago
No description
jcotton42
jcotton428mo ago
plug that code into sharplab and look at the decompile maybe?
WEIRD FLEX
WEIRD FLEX8mo ago
but it's passing through a layer which would be action(parameter)
br4kejet
br4kejet8mo ago
Even in release too
jcotton42
jcotton428mo ago
oh, I think I see what it's doing it's caching look at the invocation line
Run(<>c.<>9__0_0 ?? (<>c.<>9__0_0 = new Action<string>(<>c.<>9.<M>b__0_0)), "ok");
Run(<>c.<>9__0_0 ?? (<>c.<>9__0_0 = new Action<string>(<>c.<>9.<M>b__0_0)), "ok");
basically it's making sure the Action is only ever allocated once per the app's life instead of every time M is called
br4kejet
br4kejet8mo ago
I guess the plugin is giving the warning for the wrong reason then? Even in the IL viewer it doesn't seem to create a closure class... probably should have looked at that first
jcotton42
jcotton428mo ago
yeah
br4kejet
br4kejet8mo ago
I get the caching of the action though
jcotton42
jcotton428mo ago
Run(x => Console.WriteLine(s), "ok");
Run(x => Console.WriteLine(x), "ok");
Run(x => Console.WriteLine(x), "ok");
Run(Console.WriteLine, "ok");
Run(x => Console.WriteLine(s), "ok");
Run(x => Console.WriteLine(x), "ok");
Run(x => Console.WriteLine(x), "ok");
Run(Console.WriteLine, "ok");
becomes
<>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
<>c__DisplayClass0_.s = s;
Run(new Action<string>(<>c__DisplayClass0_.<M>b__0), "ok");
Run(<>c.<>9__0_1 ?? (<>c.<>9__0_1 = new Action<string>(<>c.<>9.<M>b__0_1)), "ok");
Run(<>c.<>9__0_2 ?? (<>c.<>9__0_2 = new Action<string>(<>c.<>9.<M>b__0_2)), "ok");
Run(<>O.<0>__WriteLine ?? (<>O.<0>__WriteLine = new Action<string>(Console.WriteLine)), "ok");
<>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
<>c__DisplayClass0_.s = s;
Run(new Action<string>(<>c__DisplayClass0_.<M>b__0), "ok");
Run(<>c.<>9__0_1 ?? (<>c.<>9__0_1 = new Action<string>(<>c.<>9.<M>b__0_1)), "ok");
Run(<>c.<>9__0_2 ?? (<>c.<>9__0_2 = new Action<string>(<>c.<>9.<M>b__0_2)), "ok");
Run(<>O.<0>__WriteLine ?? (<>O.<0>__WriteLine = new Action<string>(Console.WriteLine)), "ok");
only the first one that captures s actually incurs a new allocation on every call of M the rest are a one-time cost
333fred
333fred8mo ago
These types of warnings are notoriously hard to get right
br4kejet
br4kejet8mo ago
Hm in my actual code, it actually does seem to create an instance of one of those DisplayClass things
jcotton42
jcotton428mo ago
side note, since you're here @333fred, why are they called display classes?
333fred
333fred8mo ago
¯\_(ツ)_/¯
br4kejet
br4kejet8mo ago
Nvm i was misreading the IL code, It's storing that cached action in a display class instance. newobj only gets called twice at the start, then only once afterwards which is the ValueTuple At least my attempts to make this app run faster weren't a total waste