-1

While reviewing code last night I found this code. Of note, state is captured via the lambda which executes the generic-typed callback action.

public static void Post<TState>(TState state, Action<TState> callback){

    if(SynchronizationContext.Current is SynchronizationContext currentContext)
        // Use capture-semantics for state
        currentContext.Post(_ => callback(state), null);
    else{
        callback(state);
    }
}

Post however, natively supports pass-thru of state, so the above can also be re-written like this...

public static void Post<TState>(TState state, Action<TState> callback){

    if(SynchronizationContext.Current is SynchronizationContext currentContext)
        // Use pass-thru state, not 'capture'
        currentContext.Post(passedBackState => callback((TState)passedBackState), state);
    else{
        callback(state);
    }
}

My question is simple... in this particular use-case where the callback is defined in the same scope as the state needing to be passed to it, is there any technical benefit and/or down-side to using capture-semantics (the first) over pass-thru (the second)? Personally I like the first because there's only one variable, but I'm asking about a technical difference, if any as both seem to work.

Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286
  • I flagged as opinion based, but regarding compiler optimizations, no, the C# compiler does not perform any optimization across methods, this relates to the JIT and I'm almost sure that it will optimize both to the same code – Chayim Friedman Jan 19 '21 at 19:59
  • Yeah, just updated it to highlight I want a ***technical*** difference. Again, both work. Interesting about the allocation tho. – Mark A. Donohoe Jan 19 '21 at 20:00
  • What do you mean by "technical difference"? The generated code (IL, I haven't checked assembly) is difference, yes. But does this matter? No. – Chayim Friedman Jan 19 '21 at 20:01
  • By *technical*, I mean some negative/non-performant difference that I'm unaware of. For instance, I understand the logic of what capturing does, but I don't know if there's technical overhead. Granted, this is more for informational exploration as a single allocation is really a non-issue, but I like to understand what's going on under the hood. I'd us ILDasm, but I don't understand how to read it! lol – Mark A. Donohoe Jan 19 '21 at 20:02
  • @MarkA.Donohoe there is a technical difference. A closure allocates another object while passing the state doesn't. That extra object will have to be garbage collected, eating up CPU. For high-traffic or long-lived web services, the cost adds up. Avoiding such allocations is one of the reasons .NET Core is so much faster than .NET Old – Panagiotis Kanavos Jan 19 '21 at 20:03
  • 1
    You need a cast as `Post()` takes and sends an `object` – Chayim Friedman Jan 19 '21 at 20:05
  • 1
    @MarkA.Donohoe if you add [RoslynClrHeapAllocationAnalyzer](https://github.com/microsoft/RoslynClrHeapAllocationAnalyzer) you'll receive warnings for all such allocations. – Panagiotis Kanavos Jan 19 '21 at 20:05
  • @PanagiotisKanavos, thanks! That's exactly what I was looking for. If you put that in an answer, I'll mark it as such. – Mark A. Donohoe Jan 19 '21 at 20:05
  • @PanagiotisKanavos The jit may optimize it away – Chayim Friedman Jan 19 '21 at 20:06
  • @ChayimFriedman, added the cast. I tried to simplify the actual code so I typed that here before double-checking. Corrected. Thanks! :) – Mark A. Donohoe Jan 19 '21 at 20:10

1 Answers1

1

Closures allocate a new object on the heap that needs to be garbage collected. Passing the data through the state parameter doesn't allocate (unless the data is a value type that needs to be boxed).

These allocations can add up on frequently used methods or long-lived processes like web applications, resulting in wasted CPU cycles for garbage processing and delays. Aggressively removing such allocations is one reason .NET Core is so much faster than .NET Old.

Roslyn analyzers like the Roslyn Heap Allocation Analyzer highlight such implicit allocations like boxing a value type, closures, using params arrays etc.

Update

Rider's Dynamic Program Analysis also highlights allocations due to closures, boxing, enumerators etc.

Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
  • Awesome! I'll have to dig into that analyzer you linked to. Looks quite interesting! Shame `Post` doesn't have a generic variant since then it would just be `currentContext.Post(callback, state);` which is so succinct and beautiful! :) – Mark A. Donohoe Jan 19 '21 at 21:28