Andrey Nasonov
Andrey Nasonov

Reputation: 2629

C# generics performance vs interface

Consider the following C# code:

interface IFace
{
    void Do();
}

class Foo: IFace
{
    public void Do() { /* some action */ }
}

class Bar
{
    public void A(Foo foo) 
    {
        foo.Do();
    }

    public void B<T>(T foo)
        where T: IFace
    {
        foo.Do();
    }

    public void C(IFace foo)
    {
        foo.Do();
    }

    public void D<T>(T foo)
        where T: class, IFace
    {
        foo.Do();
    }
}

with the following usage:

Foo foo = new Foo();
Bar bar = new Bar();

MeasureExecutionTime(() => bar.A(foo), "A");
MeasureExecutionTime(() => bar.B(foo), "B");
MeasureExecutionTime(() => bar.C(foo), "C");
MeasureExecutionTime(() => bar.D(foo), "D");

The results (VS2015, .NET 4.5.2) are:

A: 3,00 ns/op, 333,4 mop/s

B: 5,74 ns/op, 174,3 mop/s

C: 5,55 ns/op, 180,3 mop/s

D: 5,64 ns/op, 177,4 mop/s

I want to know why using the generic method B has completely no advantage over using interface in both x86 and x64 modes (like C++ templates vs virtual calls). The generic method is even slightly slower than non-generic interface-based method (this effect is steady and remains when B and C measurements are swapped).

Appendix: MeasureExecutionTime code can be found here: https://gist.github.com/anonymous/9d60f5d09868ed3a00ec00f413f6afb0

Update: I have tested the code on Mono, the results are the following:

andrew@ubuntu-nas:/data/mono/json/x64$ mono Test.exe

A: 3.40 ns/op, 294.0 mop/s

B: 3.40 ns/op, 293.7 mop/s

C: 6.80 ns/op, 147.1 mop/s

D: 3.40 ns/op, 294.2 mop/s

The generated IL code can be found here: https://gist.github.com/anonymous/58df84eda906e83c64ce1b4fdc5497fb

MS and Mono generates the same IL code except for the method D. Nevertheless, it cannot explain the difference for the method B. If I run MS-generated code by Mono without recompilation, the results for the method D become the same as for B.

Upvotes: 3

Views: 1765

Answers (3)

Eric Lippert
Eric Lippert

Reputation: 660513

I want to know why using the generic method B has completely no advantage over using interface in both x86 and x64 modes (like C++ templates vs virtual calls).

CLR generics are not C++ templates.

Templates are basically a search-and-replace mechanism; if you have ten instantiations of a template then ten copies of the source code are generated and all compiled and optimized. This trades off improved optimizations at compile time against increased compile time and increased binary size.

Generics, by contrast, are compiled once to IL by the C# compiler, and then code is generated for each instantiation of the generic by the jitter. However, as an implementation detail, all instantiations that give reference types for the type arguments use the same generated code. So if you have a method C<T>.M(T t), and it is called with T being both string and IList, then the x86 (or whatever) code is generated once and used for both cases.

Therefore there is no getting around any penalty imposed by virtual function invocations or interface invocations. (Which use similar but somewhat different mechanisms.) If, say T.ToString() is called inside the method, then the jitter does not say "oh, I happen to know that if T is string then ToString is an identity; I will elide the virtual function call", or inline the body, or any such thing.

This optimization trades off decreased jit time and smaller memory usage for slightly slower invocations.

If that performance tradeoff is not the one you want, then don't use generics, interfaces or virtual function calls.

Upvotes: 9

George Helyar
George Helyar

Reputation: 5318

If you compile and look at the IL you will see that the generic version is exactly the same as the non-generic interface version, with an additional type constraint check first, which makes it slightly slower, although the difference is likely negligible in real code anyway. Even the virtual call on an interface makes a much bigger difference, and clean code is usually going to be much more important than a nanosecond here or there.

https://msdn.microsoft.com/en-us/library/system.reflection.emit.opcodes.constrained(v=vs.110).aspx

Bar.A:
IL_0000:  ldarg.1     
IL_0001:  callvirt    UserQuery+Foo.Do
IL_0006:  ret         

Bar.B:
IL_0000:  ldarga.s    01 
IL_0002:  constrained. 01 00 00 1B 
IL_0008:  callvirt    UserQuery+IFace.Do
IL_000D:  ret         

Bar.C:
IL_0000:  ldarg.1     
IL_0001:  callvirt    UserQuery+IFace.Do
IL_0006:  ret   

Generics in .net are not the same thing as templates in C++.

Upvotes: 1

VidasV
VidasV

Reputation: 4895

I think that is because you are incurring both Interface constraint checks and generic itself, compared to just passing a interface typed parameter. The difference though is not that big.

Upvotes: 0

Related Questions