C#泛型可以用来消除虚拟函数调用吗

本文关键字:虚拟 函数调用 泛型 | 更新日期: 2023-09-27 17:57:47

我同时使用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内部,每次访问列表。Count and list[i]导致一个虚拟函数调用。如果这是使用模板的C++,那么在对Foo2的调用中,编译器将能够看到虚拟函数调用可以被省略和内联,因为具体类型在模板实例化时是已知的。

但这同样适用于C#和泛型吗?当您调用Foo2(l)时,在编译时就知道T是一个List,因此也是那个列表。Count和list[i]不需要涉及虚拟函数调用。首先,这是否是一个有效的优化,不会严重破坏某些东西?如果是这样的话,编译器/JIT是否足够聪明,可以进行这种优化?

C#泛型可以用来消除虚拟函数调用吗

这是一个有趣的问题,但不幸的是,你"欺骗"系统的方法不会提高程序的效率。如果可以的话,编译器可以相对轻松地为我们做这件事!

当通过接口引用调用IList<T>时,方法是在运行时调度的,因此不能内联,这是正确的。因此,对IList<T>方法(如Count和索引器)的调用将通过接口调用。

另一方面,通过将其重写为泛型方法,您不能获得任何性能优势(至少在当前的C#编译器和.NET4CLR中没有)。

为什么不呢?首先是一些背景。C#泛型的工作是编译器编译具有可替换参数的泛型方法,然后在运行时用实际参数替换它们。这你已经知道了。

但是,该方法的参数化版本对变量类型的了解并不比您和我在编译时所了解的更多。在这种情况下,编译器所知道的关于Foo2的只是listIList<int>。我们在通用Foo2中具有与在非通用Foo1中相同的信息。

事实上,为了避免代码膨胀,JIT编译器只为所有引用类型生成泛型方法的单个实例化。以下是描述这种替换和实例化的Microsoft文档:

如果客户端指定了引用类型,则JIT编译器将服务器IL中的泛型参数替换为Object,并将其编译为本机代码。该代码将用于对引用类型而不是泛型类型参数的任何进一步请求。注意,通过这种方式,JIT编译器只重用实际的代码。实例仍然根据它们在托管堆之外的大小进行分配,并且没有强制转换。

这意味着JIT编译器的方法版本(对于引用类型)是不是类型安全的,但这并不重要,因为编译器在编译时已经确保了所有类型的安全。但更重要的是,对于您的问题,没有任何途径可以执行内联并提高性能。

编辑:最后,根据经验,我刚刚对Foo1Foo2进行了基准测试,它们产生了相同的性能结果。换句话说,Foo2Foo1而不是

让我们添加一个"可内联"版本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的速度。

这确实有效,并且(如果函数不是虚拟的)会导致非虚拟调用。原因是与C++不同,CLR泛型在JIT时为每个唯一的泛型参数集定义了一个特定的具体类(通过尾部1、2等的反射来指示)。如果该方法是虚拟的,它将像任何具体的、非虚拟的、非泛型的方法一样产生虚拟调用。

关于.net泛型需要记住的是给定的:

Foo<T>; 

然后

Foo<Int32>

是运行时的有效类型,与分离且不同

Foo<String>

,并相应地处理所有虚拟和非虚拟方法。这就是为什么你可以创建一个

List<Vehicle>

并添加一辆汽车,但你不能创建类型的变量

List<Vehicle> 

并将其值设置为的实例

List<Car>

它们有不同的类型,但前者有一个Add(...)方法,它接受Vehicle的自变量,Car的超类型。