简体   繁体   English

C# generics 可以用来省略虚拟 function 调用吗?

[英]Can C# generics be used to elide virtual function calls?

I use both C++ and C# and something that's been on my mind is whether it's possible to use generics in C# to elide virtual function calls on interfaces. I use both C++ and C# and something that's been on my mind is whether it's possible to use generics in C# to elide virtual function calls on interfaces. Consider the following:考虑以下:

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);

Inside Foo1, every access to list.Count and list[i] causes a virtual function call.在 Foo1 内部,对 list.Count 和 list[i] 的每次访问都会导致虚拟 function 调用。 If this were C++ using templates, then in the call to Foo2 the compiler would be able to see that the virtual function call can be elided and inlined because the concrete type is known at template instantiation time.如果这是使用模板的 C++,那么在对 Foo2 的调用中,编译器将能够看到虚拟 function 调用可以被省略和内联,因为具体类型在模板实例化时是已知的。

But does the same apply to C# and generics?但同样适用于 C# 和 generics 吗? When you call Foo2(l), it's known at compile-time that T is a List and therefore that list.Count and list[i] don't need to involve virtual function calls.当您调用 Foo2(l) 时,在编译时就知道 T 是一个 List,因此 list.Count 和 list[i] 不需要涉及虚拟 function 调用。 First of all, would that be a valid optimization that doesn't horribly break something?首先,这将是一个不会严重破坏某些东西的有效优化吗? And if so, is the compiler/JIT smart enough to make this optimization?如果是这样,编译器/JIT 是否足够聪明,可以进行这种优化?

This is an interesting question, but unfortunately, your approach to "cheat" the system won't improve the efficiency of your program.这是一个有趣的问题,但不幸的是,您“欺骗”系统的方法不会提高程序的效率。 If it could, the compiler could do it for us with relative ease!如果可以,编译器可以相对轻松地为我们完成它!

You are correct that when calling IList<T> through an interface reference, that the methods are dispatched at runtime and therefore cannot be inlined.您是正确的,当通过接口引用调用IList<T>时,方法是在运行时调度的,因此不能内联。 Therefore the calls to IList<T> methods such as Count and the indexer will be called through the interface.因此,对IList<T>方法(如Count和索引器)的调用将通过接口调用。

On the other hand, it is not true that you can achieve any performance advantage (at least not with the current C# compiler and .NET4 CLR), by rewriting it as a generic method.另一方面,通过将其重写为通用方法,您不能获得任何性能优势(至少不能使用当前的 C# 编译器和 .NET4 CLR)。

Why not?为什么不? First some background.首先是一些背景。 The C# generics work is that the compiler compiles your generic method that has replaceable parameters and then replaces them at run-time with the actual parameters. C# generics 的工作是编译器编译具有可替换参数的泛型方法,然后在运行时用实际参数替换它们。 This you already knew.这个你已经知道了。

But the parameterized version of the method knows no more about the variable types than you and I do at compile time.但是该方法的参数化版本对变量类型的了解并不比您和我在编译时更多。 In this case, all the compiler knows about Foo2 is that list is an IList<int> .在这种情况下,编译器对Foo2的所有了解就是list是一个IList<int> We have the same information in the generic Foo2 that we do in the non-generic Foo1 .我们在泛型Foo2中拥有与在非泛型Foo1中相同的信息。

As a matter of fact, in order to avoid code-bloat, the JIT compiler only produces a single instantiation of the generic method for all reference types .事实上,为了避免代码膨胀,JIT 编译器只为所有引用类型生成一个泛型方法的实例化。 Here is the Microsoft documentation that describes this substitution and instantiation:以下是描述此替换和实例化的Microsoft 文档

If the client specifies a reference type, then the JIT compiler replaces the generic parameters in the server IL with Object, and compiles it into native code.如果客户端指定引用类型,则 JIT 编译器将服务器 IL 中的泛型参数替换为 Object,并将其编译为本机代码。 That code will be used in any further request for a reference type instead of a generic type parameter.该代码将用于对引用类型而不是泛型类型参数的任何进一步请求。 Note that this way the JIT compiler only reuses actual code.请注意,这种方式 JIT 编译器仅重用实际代码。 Instances are still allocated according to their size off the managed heap, and there is no casting.仍然根据托管堆的大小分配实例,并且没有强制转换。

This means that the JIT compiler's version of the method (for reference types) is not type safe but it doesn't matter because the compiler has ensured all type-safety at compile time.这意味着 JIT 编译器的方法版本(对于引用类型)不是类型安全的,但这并不重要,因为编译器在编译时已确保所有类型安全。 But more importantly for your question, there is no avenue to perform inlining and get a performance boost.但更重要的是,对于您的问题,没有办法执行内联并获得性能提升。

Edit: Finally, empirically, I've just done a benchmark of both Foo1 and Foo2 and they yield identical performance results.编辑:最后,凭经验,我刚刚对Foo1Foo2进行了基准测试,它们产生了相同的性能结果。 In other words, Foo2 is not any faster than Foo1 .换句话说, Foo2并不Foo1快。

Let's add an "inlinable" version Foo0 for comparison:让我们添加一个“可内联”版本Foo0进行比较:

int Foo0(List<int> list)
{
    int sum = 0;
    for (int i = 0; i < list.Count; ++i)
        sum += list[i];
    return sum;
}

Here is the performance comparison:下面是性能对比:

Foo0 = 1719
Foo1 = 7299
Foo2 = 7472
Foo0 = 1671
Foo1 = 7470
Foo2 = 7756

So you can see that Foo0 , which can be inlined, is dramatically faster than the other two.因此,您可以看到可以内联的Foo0比其他两个快得多。 You can also see that Foo2 is slightly slower instead of being anywhere near as fast as Foo0 .您还可以看到Foo2稍微慢一些,而不是像Foo0一样快。

This actually does work, and does (if the function is not virtual) result in a non-virtual call.这实际上确实有效,并且(如果 function 不是虚拟的)会导致非虚拟调用。 The reason is that unlike in C++, CLR generics define, at JIT time, a specific, concrete class for each unique set of generic parameters (indicated via reflection via trailing 1, 2 etc).原因是,与 C++ 不同,CLR generics 在 JIT 时间定义了一个特定的具体 class,用于每个唯一的一组通用参数(通过尾随反射等)。 If the method is virtual, it will result in a virtual call like any concrete, non-virtual, non-generic method.如果方法是虚拟的,它将像任何具体的、非虚拟的、非泛型的方法一样导致虚拟调用。

The thing to remember about .net generics is that given:关于 .net generics 要记住的是:

Foo<T>; 

then然后

Foo<Int32>

is a valid Type at runtime, separate and distinct from是运行时有效的类型,与

Foo<String>

, and all virtual and non-virtual methods are treated accordingly. ,并且所有虚拟和非虚拟方法都被相应地处理。 This is the reason why you can create a这就是为什么您可以创建一个

List<Vehicle>

and add a Car to it, but you can't create a variable of type并向其中添加 Car,但您不能创建类型的变量

List<Vehicle> 

and set its value to an instance of并将其值设置为

List<Car>

. . They are of different types, but the former has an Add(...) method that takes an argument of Vehicle , a supertype of Car .它们属于不同的类型,但前者有一个Add(...)方法,该方法接受Vehicle的参数,即Car的超类型。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM