15

I found this strange behaviour in .NET and even after looking into CLR via C# again I am still confused. Let's assume we have an interface with one method and a class that imlements it:

interface IFoo
{
    void Do();
}

class TheFoo : IFoo
{
    public void Do()
    {
        //do nothing
    }
}

Then we want just to instantiate this class and call this Do() method a lot of times in two ways: using concrete class variable and using an interface variable:

TheFoo foo1 = new TheFoo();

Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (long i = 0; i < 1000000000; i++)
    foo1.Do();
stopwatch.Stop();
Console.Out.WriteLine("Elapsed time: " + stopwatch.ElapsedMilliseconds);

IFoo foo2 = foo1;

stopwatch = new Stopwatch();
stopwatch.Start();
for (long i = 0; i < 1000000000; i++)
    foo2.Do();
stopwatch.Stop();
Console.Out.WriteLine("Elapsed time: " + stopwatch.ElapsedMilliseconds);

Surprisingly (at least to me) the elapsed times are about 10% different:

Elapsed time: 6005
Elapsed time: 6667

The difference is not that much, so I would not worry a lot about this in most cases. However I just can't figure out why this happens even after looking in IL code, so I would appreciate if somebody point me to something obvious that I am missing.

Alexander Tsvetkov
  • 1,649
  • 14
  • 24
  • I ran your tests and I actually got a very different result. Elapsed time: 12125 Elapsed time: 11682. I'm obviously running on a slower machine. Measuring performance like this is tough because there are factors that may be at play that aren't clear. – Craig Suchanec Jun 18 '11 at 13:52
  • 1
    @Craig Probably you have some process interfered. I have very consistent results on this mini-benchmark for 10 times. – Ivan Danilov Jun 18 '11 at 13:56
  • @Ivan I ran it multiple times before I posted and got consistent results. I also just went and reran it putting it into a loop and running it 10 times back to back. I have very little else running on the machine and I am consistently seeing the first case being slower. Its a significant difference now, almost 2:1. I also added another class that explicitly implements the interface and ran the test on that. It ran in about the same as the second case. What version of the framework are you using? Maybe that accounts for the differences. – Craig Suchanec Jun 18 '11 at 14:13
  • I added `Console.WriteLine(Environment.Version)` and its output was 4.0.30319.235. But it is really strange. – Ivan Danilov Jun 18 '11 at 14:20
  • Mine is the same. It is very strange. Looking at this again, the first time it runs the two times are about the same. The subsequent times through the loop that's when the 2:1 ratio appears. – Craig Suchanec Jun 18 '11 at 14:29
  • Try to switch the order of the cases. Could it be some startup/jitting/whatever overhead that wasn't so apparent on more powerful boxes? – Ivan Danilov Jun 18 '11 at 14:32
  • Has anyone ever encountered a scenario where this made a significant difference at the macro level? Most performance bottlenecks I have seen involved I/O. The next most common cause is probably poor algorithms. – TrueWill Jun 18 '11 at 15:32

2 Answers2

18

You have to look at the machine code to see what is going on. When you do, you'll see that the jitter optimizer has completely removed the call to foo1.Do(). Small methods like that get inlined by the optimizer. Since the body of the method contains no code, no machine code is generated at all. It cannot make the same optimization on the interface call, it is not quite smart enough to reverse-engineer that the interface method pointer actually points to an empty method.

Check this answer for a list of the common optimizations performed by the jitter. Note the warnings about profiling mentioned in that answer.

NOTE: looking at the machine code in release build requires changing an option. By default the optimizer is disabled when you debug code, even in the release build. Tools + Options, Debugging, General, untick "Suppress JIT optimization on module load".

Community
  • 1
  • 1
Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • The thing is I actually saw resulting assembler code in the debugger. It wasn't optimized out by jitter. But it is a good point anyway. +1 – Ivan Danilov Jun 18 '11 at 14:42
  • To exclude optimization influence I've just included static int variable that is incremented on each Do() call. And at the end `x` goes to console, so that compiler couldn't eliminate it as well. Timings become slightly higher but trend is the same nevertheless. – Ivan Danilov Jun 18 '11 at 14:46
  • @Ivan: *How* did you see that the resulting code wasn't optimized out by the jitter? People often do this wrong. What you have to do is ensure that the code is jitted *before* you attach the debugger. The jitter knows whether a debugger is attached or not, and gets less aggressive about optimizations that make it hard to set breakpoints if it knows that a debugger is attached. – Eric Lippert Jun 18 '11 at 15:55
  • Covered by the NOTE in the answer. – Hans Passant Jun 18 '11 at 16:04
  • @Eric I started it with breakpoint, went to Disassembly and saw actual `call` instructions. Yes, I understand that optimizations could remove them on actual run. That's why I also added 'visible' output mentioned in my previous comment to this answer, so that my Do() changed static variable and cannot be eliminated (at least I believe that jit can't do this sort of optimizations yet). This didn't influence the result (timings were changed but more or less proportionally) so I concluded that interface mapping DO introduce some overhead and it was stated in Vance's linked blog post also. – Ivan Danilov Jun 18 '11 at 16:41
1

Well, compiler can't figure in general case, which actual method body should be executed when interface method is called because different classes could have different implementations.

So, when CLR faces interface call it sees at interface mappings of enclosing type and checks which concrete method it should call. It's lower than IL, actually.

UPD: IMO it is not the difference between call and callvirt.

What should CLR do when it encounters callvirt on a class type? Get type of the callee, look at its virtual methods table, find there method being called and call it.

What should it do when it encounters callvirt on an interface type? Well, in addition to prev points it should also check such things as explicit interface implementation. Because you COULD have two methods with identical signatures - one is class' method and other is explicit interface implementation. Such thing just do not exist when dealing with class types. I think it is the main difference here.

UPD2: Now I'm sure it is the case. See this for actual implementation detail.

Ivan Danilov
  • 14,287
  • 6
  • 48
  • 66