我同时使用C ++和C#,我想到的是,是否可以在C#中使用泛型来消除接口上的虚函数调用。请考虑以下事项:
int Foo1(IList<int> list)
{
int sum = 0;
for(int i = 0; i < list.Count; ++i)
sum += list[i];
return sum;
}
int Foo2<T>(T list) where T : IList<int>
{
int sum = 0;
for(int i = 0; i < list.Count; ++i)
sum += list[i];
return sum;
}
/*...*/
var l = new List<int>();
Foo1(l);
Foo2(l);
在Foo1内部,每次访问list.Count和list [i]都会导致虚函数调用。如果这是使用模板的C ++,那么在调用Foo2时,编译器将能够看到虚拟函数调用可以被省略和内联,因为具体类型在模板实例化时已知。
但这同样适用于C#和泛型吗?当你调用Foo2(l)时,在编译时就知道T是List,因此list.Count和list [i]不需要涉及虚函数调用。首先,这是一个有效的优化,并没有可怕的破坏?如果是这样,编译器/ JIT是否足够聪明以进行此优化?
答案 0 :(得分:8)
这是一个有趣的问题,但遗憾的是,您“欺骗”系统的方法不会提高程序的效率。如果可以的话,编译器可以相对轻松地为我们做到这一点!
通过接口引用调用IList<T>
时,你是正确的,方法是在运行时调度的,因此无法内联。因此,将通过接口调用IList<T>
方法(如Count
和索引器的调用。
另一方面,通过将其重写为通用方法,您无法获得任何性能优势(至少不能使用当前的C#编译器和.NET4 CLR)。
为什么不呢?首先是一些背景。 C#泛型工作是编译器编译具有可替换参数的泛型方法,然后在运行时使用实际参数替换它们。你已经知道了。
但是该方法的参数化版本不再了解您和我在编译时所做的变量类型。在这种情况下,所有编译器都知道Foo2
是list
是IList<int>
。我们在非通用Foo2
中执行的通用Foo1
中包含相同的信息。
事实上,为了避免代码膨胀,JIT编译器只为所有引用类型生成通用方法的单个实例。以下是描述此替换和实例化的Microsoft documentation:
如果客户端指定了引用类型,则JIT编译器用Object替换服务器IL中的泛型参数,并将其编译为本机代码。该代码将用于对引用类型的任何进一步请求,而不是泛型类型参数。请注意,这样JIT编译器只重用实际代码。实例仍然根据托管堆的大小进行分配,并且没有转换。
这意味着JIT编译器的方法版本(对于引用类型)是不是类型安全的,但无关紧要,因为编译器在编译时确保了所有类型安全性。但更重要的是,对于你的问题,没有办法进行内联并获得性能提升。
编辑:最后,根据经验,我刚刚完成了Foo1
和Foo2
的基准测试,他们会产生相同的效果结果。换句话说,Foo2
不的速度比Foo1
快。
让我们添加一个“inlinable”版本Foo0
进行比较:
int Foo0(List<int> list)
{
int sum = 0;
for (int i = 0; i < list.Count; ++i)
sum += list[i];
return sum;
}
以下是效果比较:
Foo0 = 1719
Foo1 = 7299
Foo2 = 7472
Foo0 = 1671
Foo1 = 7470
Foo2 = 7756
因此,您可以看到可以内联的Foo0
比其他两个快得多。您还可以看到Foo2
略慢,而不是像Foo0
那样快。
答案 1 :(得分:4)
这实际上确实有效,并且(如果该功能不是虚拟的)会导致非虚拟呼叫。原因在于,与C ++不同,CLR泛型在JIT时为每个唯一的通用参数集定义一个特定的具体类(通过尾随1,2等反射表示)。如果该方法是虚拟的,它将导致虚拟调用,如任何具体的,非虚拟的,非泛型的方法。
关于.net泛型需要记住的事情是:
Foo<T>;
然后
Foo<Int32>
是运行时的有效类型,与
分开且不同Foo<String>
,并相应地处理所有虚拟和非虚拟方法。这就是你可以创建
的原因List<Vehicle>
并向其添加Car,但您无法创建
类型的变量List<Vehicle>
并将其值设置为
的实例List<Car>
。它们的类型不同,但前者有一个Add(...)
方法,其参数为Vehicle
,超类型为Car
。