C#泛型性能与接口
本文关键字:接口 性能 泛型 | 更新日期: 2023-09-27 17:57:56
考虑以下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();
}
}
具有以下用途:
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");
结果(VS2015,.NET 4.5.2)为:
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
我想知道为什么在x86和x64模式下使用通用方法B
完全没有使用接口的优势(比如C++模板与虚拟调用)。通用方法甚至比基于非通用接口的方法稍慢(这种效果是稳定的,并且在交换B和C测量值时保持不变)。
附录:MeasureExecutionTime代码可在此处找到:https://gist.github.com/anonymous/9d60f5d09868ed3a00ec00f413f6afb0
更新:我已经在Mono上测试了代码,结果如下:
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
生成的IL代码可在此处找到:https://gist.github.com/anonymous/58df84eda906e83c64ce1b4fdc5497fb
除了方法D
之外,MS和Mono生成相同的IL代码。然而,它不能解释方法B
的差异。如果我在不重新编译的情况下运行Mono生成的MS代码,那么方法D
的结果将与B
的结果相同。
我想知道为什么在x86和x64模式下使用通用方法B完全没有使用接口的优势(比如C++模板与虚拟调用)。
CLR泛型不是C++模板。
模板基本上是一种搜索和替换机制;如果您有一个模板的十个实例,那么将生成源代码的十个副本,并对其进行编译和优化。这在编译时的改进优化与增加的编译时和增加的二进制大小之间进行了权衡。
相反,泛型由C#编译器编译一次到IL,然后通过抖动为泛型的每个实例化生成代码然而,作为实现细节,所有为类型参数提供引用类型的实例化都使用相同的生成代码。因此,如果您有一个方法C<T>.M(T t)
,并且它是在T既是字符串又是IList的情况下调用的,那么x86(或其他)代码将生成一次并用于这两种情况。
因此,虚拟函数调用或接口调用不会带来任何惩罚。(它们使用类似但有些不同的机制。)如果T.ToString()
在方法内部被调用,那么抖动不会说"哦,我碰巧知道如果T是字符串,那么ToString就是一个身份;我将省略虚拟函数调用",或者内联主体,或者任何类似的事情。
这种优化以降低的jit时间和较小的内存使用量换取稍慢的调用。
如果性能折衷不是您想要的,那么就不要使用泛型、接口或虚拟函数调用。
如果你编译并查看IL,你会发现泛型版本与非泛型接口版本完全相同,首先进行了额外的类型约束检查,这会使它稍微慢一些,尽管在实际代码中这种差异可能可以忽略不计。即使是对接口的虚拟调用也会产生更大的影响,而干净的代码通常比一纳秒更重要。
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
net中的泛型与C++中的模板不是一回事。
我认为这是因为与只传递接口类型的参数相比,您要同时进行接口约束检查和泛型本身。不过差别并没有那么大。