Why is casting to a generic type slower than an explicit cast in C#?
Asked Answered
F

2

6

I'm building a message dispatch map in C# and mostly just playing around with some different approaches. I am curious about a performance difference I am measuring, but it's not obvious why from looking at the IL.

The message map:

delegate void MessageHandler(Message message);
AddHandler(Type t, MessageHandler handler) 
{ 
    /* add 'handler' to messageMap invocation list */ 
}

delegate void GenericMessageHandler<T>(T message);
AddHandler<T>(GenericMessageHandler<T> handler) where T: Message
{
    AddHandler(typeof(T), e => { handler((T)e); });
}

Dictionary<Type, MessageHandler> messageMap;

I then have a class hierarchy of Messages, similar to EventArgs in WPF, for example:

public class Message {}
public class VelocityUpdateMessage : Message

and observer classes with handler functions:

void HandleVelocityUpdate(VelocityUpdateMessage message) { ... }

I am measuring 2 ways of adding & invoking handlers. I am wrapping the delegate call so I can get a bit of conceptual type safety and therein lies the perf difference.

Approach 1: listener calls

AddHandler(typeof(VelocityUpdateMessage), 
           e => { HandleVelocityUpdate((VelocityUpdateMessage)e); });

Approach 2: listener calls

AddHandler<VelocityUpdateMessage>(HandleVelocityUpdate);

Both approaches build a MessageHandler delegate that makes a cast and the same method call, but calling the delegates built using approach #2 is a wee bit slower even though the generated IL looks identical. Is it extra runtime overhead in casting to a generic type? Is it the type constraint? I would expect the JITted delegates to be the same once the generic type is resolved.

Thanks for any info.

Focus answered 21/1, 2012 at 8:47 Comment(1)
How are you measuring? That is extremely important with these micro-optimizations.Module
F
0

ok, I had to look at MethodBody.GetILAsByteArray() IL rather than the ILSpy results for the delegates to get to the bottom of this. Using a generic delegate to wrap my message handler and cast the message type generates:

0000 : ldarg.0
0001 : ldfld
0006 : ldarg.1
0007 : unbox.any
000C : callvirt void MessageTest.Message+tMessageHandler`1[MessageTest.VelocityUpdateMessage].Invoke(‌​MessageTest.VelocityUpdateMessage) 
0011 : ret

where the wrapper delegate with the explicit cast generates:

0000 : ldarg.0
0001 : ldarg.1
0002 : castclass
0007 : call void Message.Component.HandleVelocityUpdate(MessageTest.VelocityUpdateMessage)
000C : ret

So yes, there is minimal overhead from using generics in this way.

Focus answered 21/1, 2012 at 21:25 Comment(0)
I
3

The below line creates a new instance of an anonymous type each time it is called. Could that the cause of your performance difference?

AddHandler(typeof(T), e => { handler((T)e); }); 
Inseverable answered 21/1, 2012 at 8:54 Comment(4)
The line doesn't contain a new { ... }. Where is the anonymous type?Gyroscope
To clarify, I am seeing the performance difference when calling the delegates (dispatching the messages to the handler), I am not profiling the AddHandler calls since they are setup code. As Christopher says I will get a new method jitted for each type but my test code is just calling the same handler over and over in a tight loop so the hit for code generation should be just a small blip. Maybe its more than I think it is. I'm seeing an elapsed time of ~70ms for 100k calls of #1 vs. ~110ms for #2. It's not orders of magnitude like calling Delegate.DynamicInvoke.Focus
It is not an anonymous type, at least not in the formal C# language definition of one. There is a hidden class that makes the lambda expression work, the new expression is hidden as well. The OP makes the opposite observation though.Rudder
Hans Passant is correct -- that is not an "anonymous type" insofar as it is not a usage of the C# language feature called "anonymous types". It is an anonymous type in the more general sense that it is a compiler-generated type with an "unspeakable" name. We usually call such types "closure classes", or "display classes". (The latter is an unfortunate bit of jargon; the type we generate contains elements that allow the debugger to display the members of the type as locals rather than fields, so it is the "display" class.)Penick
F
0

ok, I had to look at MethodBody.GetILAsByteArray() IL rather than the ILSpy results for the delegates to get to the bottom of this. Using a generic delegate to wrap my message handler and cast the message type generates:

0000 : ldarg.0
0001 : ldfld
0006 : ldarg.1
0007 : unbox.any
000C : callvirt void MessageTest.Message+tMessageHandler`1[MessageTest.VelocityUpdateMessage].Invoke(‌​MessageTest.VelocityUpdateMessage) 
0011 : ret

where the wrapper delegate with the explicit cast generates:

0000 : ldarg.0
0001 : ldarg.1
0002 : castclass
0007 : call void Message.Component.HandleVelocityUpdate(MessageTest.VelocityUpdateMessage)
000C : ret

So yes, there is minimal overhead from using generics in this way.

Focus answered 21/1, 2012 at 21:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.