C#泛型性能与接口

And*_*nov 2 c# generics

考虑以下C#代码:

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

具有以下用途:

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");
Run Code Online (Sandbox Code Playgroud)

结果(VS2015,.NET 4.5.2)是:

答: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

我想知道为什么使用泛型方法B在x86和x64模式(如C++模板与虚拟调用)中使用接口完全没有优势.通用方法甚至比非通用的基于接口的方法稍慢(这种效果是稳定的,并且在交换B和C测量时保持不变).

附录:MeasureExecutionTime代码可以在这里找到:https://gist.github.com/anonymous/9d60f5d09868ed3a00ec00f413f6afb0

更新:我在Mono上测试了代码,结果如下:

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

答: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

生成的IL代码可以在这里找到:https: //gist.github.com/anonymous/58df84eda906e83c64ce1b4fdc5497fb

除方法外,MS和Mono生成相同的IL代码D.然而,它无法解释该方法的差异B.如果我在没有重新编译的情况下通过Mono运行MS生成的代码,则该方法的结果D将与for相同B.

Eri*_*ert 7

我想知道为什么使用泛型方法B在x86和x64模式(如C++模板与虚拟调用)中使用接口完全没有优势.

CLR泛型不是C++模板.

模板基本上是一种搜索和替换机制; 如果您有十个模板实例,则会生成十个源代码副本,并进行全部编译和优化.这样可以在编译时改进优化,以防止增加编译时间和增加二进制文件大小.

相反,泛型由C#编译器编译一次到IL,然后通过抖动为通用的每个实例化生成代码. 但是,作为实现细节,为类型参数提供引用类型的所有实例都使用相同的生成代码.因此,如果你有一个方法C<T>.M(T t),并且它被调用时T既是字符串又是IList,那么x86(或其他)代码只生成一次并用于两种情况.

因此,虚拟函数调用或接口调用不会产生任何惩罚.(使用类似但有些不同的机制.)如果,比如T.ToString()在方法内部调用,则抖动不会说"哦,我碰巧知道如果T是字符串,那么ToString是一个标识;我将忽略虚函数调用",或者内联身体或任何此类事物.

此优化会减少jit时间,减少内存使用量,以减少稍慢的调用.

如果性能权衡不是您想要的,那么不要使用泛型,接口或虚函数调用.