Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Calling method of non-assigned class

Tags:

c#

oop

il

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?

like image 992
Emre Kantar Avatar asked Jan 26 '16 14:01

Emre Kantar


Video Answer


1 Answers

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: Generate IL that checks for null, then generate a call.
  • Case 2: Generate a callvirt.
  • Case 3: Generate a call, but do not start with a null check.

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:

  • The call is a non-virtual method call, and
  • C# already knows that the receiver is not null

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.

like image 163
Eric Lippert Avatar answered Sep 22 '22 07:09

Eric Lippert