I have doubt about these two aspects;
First one;
Test test = new Test();
result = test.DoWork(_param);
Second one;
result = new Test().DoWork(_param);
What happens if we dont assign the newly created instance to a variable and directly call the method?
I see some difference between two way on IL code.
This one below is IL output of first c# code
IL_0000: ldstr "job "
IL_0005: stloc.0
IL_0006: newobj instance void Works.Test::.ctor()
IL_000b: stloc.1
IL_000c: ldloc.1
IL_000d: ldloc.0
IL_000e: callvirt instance string Works.Test::DoWork(string)
IL_0013: pop
IL_0014: ret
And this one is the IL output of the second c# code
IL_0000: ldstr "job "
IL_0005: stloc.0
IL_0006: newobj instance void Works.Test::.ctor()
IL_000b: ldloc.0
IL_000c: call instance string Works.Test::DoWork(string)
IL_0011: pop
IL_0012: ret
Could you please inform me?
The question is a little bit hard to find here but I think what you are asking is:
why does assigning the newly created reference to a variable cause the compiler to generate a callvirt, but calling the method directly generates a call?
You are very observant to notice this subtle difference.
Before we get to your question let's answer some other questions.
Should I trust that the compiler generates good code?
Generally yes. There are occasional code gen bugs but this is not one of them.
Is it legal to call a non-virtual method with callvirt?
Yes.
Is it legal to call a virtual method with call?
Yes, if you're trying to, say, call a base class method rather than an override in a derived class. But usually this does not happen.
Is the method being called in this example virtual or not?
It's not virtual.
Since the method is not virtual, it could be called with callvirt or call. Why does the compiler sometimes generate callvirt and sometimes generate call, when it could generate callvirt both times or call both times, consistently?
Now we get to the interesting part of your question.
There are two differences between call and callvirt.
call does not do virtual dispatch; callvirt looks up the right method in the virtual function dispatch table before it calls it. So therefore callvirt is about a nanosecond slower.
callvirt always checks if the receiver is null, regardless of whether the method called is virtual or not. call does not check to see if the receiver is null. It is legal to call a method with a null "this" via call.
Now perhaps you see where this is going.
Is C# required to crash with a null dereference exception whenever a call is made on a null reference receiver?
Yes. C# is required to crash when you invoke something with a null receiver. Therefore C# has the following choices when generating code to call a method:
Case 1 is just dumb. The IL is larger, and so takes up more room on disk, is slower to load, is slower to jit. It would be foolish to generate this code when callvirt automatically does a null check.
Case 2 is smart. The C# compiler generates callvirts so that the null check is done automatically.
Now what about case 3? Under what circumstances can C# skip the null check and generate a call? Only when:
But C# knows that in new Foo().Bar()
the receiver cannot be null because if it was, then the construction would have thrown an exception and we'd never get to the call!
The compiler is not smart enough to realize that the variable has only ever been assigned non-null values. So it generates a callvirt to be safe.
The compiler could be written to be that smart. The compiler already has to track the assignment state of variables for definite assignment checking. It could also track the "was assigned something that might be null" state, and then it would generate a call in both cases. But the compiler is not (yet) that smart.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With