C#与C ++中的虚拟调用速度

我似乎记得在某个地方读过C#虚拟调用的成本并不像在C ++中那么高。 这是真的? 如果是这样 – 为什么?

AC#虚拟调用必须检查“this”是否为空,而C ++虚拟调用不是。 所以我一般不会看到为什么C#虚拟调用会更快。 在特殊情况下,C#编译器(或JIT编译器) 可能能够比C ++编译器更好地内联虚拟调用,因为C#编译器可以访问更好的类型信息。 调用方法指令有时可能在C ++中较慢,因为C#JIT可能能够使用更快的指令,只处理小偏移,因为它更多地了解运行时内存布局和处理器模型,然后是C ++编译器。

但是,我们最多只讨论一些处理器指令。 在调制解调器超标量处理器上,“空检查”指令很可能与“调用方法”同时运行,因此不需要时间。

如果在循环中调用make,则所有处理器指令很可能已经是1级高速缓存。 但是数据不太可能是缓存,这些天从主内存读取数据值的成本与从1级缓存运行100条指令的成本相同。 因此,不幸的是,在实际应用中,虚拟呼叫的成本甚至可以在很少的地方进行测量。

C#代码使用更多指令的事实当然会减少可以容纳在缓存中的代码量,这种影响无法预测。

(如果C ++类使用多个内在性,则成本更高,因为必须修补“this”指针。同样,C#中的接口添加了另一个重定向级别。)

对于JIT编译语言(我不知道CLR是否这样做,Sun的JVM会这样做),将一个只有两三个实现的虚拟调用转换为类型和直接或内联的测试序列是一种常见的优化调用。

这样做的好处是,现代流水线CPU可以使用分支预测和预取直接调用,但间接调用(由高级语言中的函数指针表示)通常会导致流水线停滞。

在极限情况下,只有一个虚拟呼叫的实现,并且呼叫的主体足够小,虚拟呼叫简化为纯内联代码 。 这种技术用于自动语言运行库,JVM是从这种运行时发展而来的。

大多数C ++编译器不执行执行此优化所需的整个程序分析,但是诸如LLVM之类的项目正在考虑诸如此类的整个程序优化。

最初的问题是:

我似乎记得在某个地方读过C#虚拟调用的成本并不像在C ++中那么高。

请注意重点。 换句话说,这个问题可以改为:

我似乎记得在某处看过,在C#中,虚拟和非虚拟调用同样很慢,而在C ++中,虚拟调用比非虚拟调用慢…

因此,在任何情况下,提问者都没有声称C#比C ++更快。

可能是无用的转移,但这引发了我对C ++的好奇心与/ clr:pure,没有使用C ++ / CLI扩展。 编译器生成的IL由JIT转换为本机代码,尽管它是纯C ++。 所以在这里我们可以看到如果在与C#相同的平台上运行,标准C ++实现会做什么。

使用非虚方法:

struct Plain { void Bar() { System::Console::WriteLine("hi"); } }; 

这段代码:

 Plain *p = new Plain(); p->Bar(); 

…导致使用特定方法名称发出call操作码,向Bar传递一个隐式的this参数。

 call void ::Plain.Bar(valuetype Plain*) 

与inheritance层次结构比较:

 struct Base { virtual void Bar() = 0; }; struct Derived : Base { void Bar() { System::Console::WriteLine("hi"); } }; 

如果我们这样做:

 Base *b = new Derived(); b->Bar(); 

这会发出calli操作码,它会跳转到计算出的地址 – 因此在调用之前会有很多IL。 通过将其重新打开到C#,我们可以看到发生了什么:

 **(*((int*) b))(b); 

换句话说,将b的地址转换为指向int的指针(恰好与指针大小相同)并获取该位置的值,即vtable的地址,然后取出第一个项目。 vtable,它是跳转到的地址,取消引用它并调用它,向它传递隐含的this参数。

我们可以调整虚拟示例以使用C ++ / CLI扩展:

 ref struct Base { virtual void Bar() = 0; }; ref struct Derived : Base { virtual void Bar() override { System::Console::WriteLine("hi"); } }; Base ^b = gcnew Derived(); b->Bar(); 

这会生成callvirt操作码,就像在C#中一样:

 callvirt instance void Base::Bar() 

因此,当编译目标CLR时,Microsoft的当前C ++编译器与使用每种语言的标准function时C#所做的优化不同; 对于标准C ++类层次结构,C ++编译器生成的代码包含用于遍历vtable的硬编码逻辑,而对于ref类,它将其留给JIT以确定最佳实现。

我猜这个假设基于JIT编译器,这意味着C#可能在实际使用之前将虚拟调用转换为一个简单的方法调用。

但它基本上是理论上的,我不会赌它!

C ++中虚拟调用的成本是通过指针(vtbl)调用函数的成本。 我怀疑C#可以更快地完成那个并且仍然能够在运行时确定对象类型…

编辑:正如Pete Kirkham指出的那样,一个好的JIT可能能够内联C#调用,避免管道停滞; 大多数C ++编译器都无法做到的事情。 另一方面,Ian Ringrose提到了对缓存使用的影响。 除此之外,JIT本身正在运行,并且(严格来说是个人)我不会打扰,除非在实际工作负载下对目标机器进行分析已certificate比另一个更快。 它充其量只是微观优化。

不确定完整的框架,但在Compact Framework中,它会更慢,因为CF没有虚拟调用表,尽管它会缓存结果。 这意味着CF中的虚拟调用在第一次调用时会变慢,因为它必须进行手动查找。 如果应用程序内存不足,每次调用它时可能会很慢,因为缓存的查找可能会被调整。

在C#中,可以通过分析代码将虚拟function转换为非虚拟function。 在实践中,它不会经常发生太大的变化。

C#展平vtable并内联祖先调用,因此您不会链接inheritance层次结构来解决任何问题。

这可能不是你的问题的答案,但尽管.NET JIT优化了所有人之前所说的虚拟调用,但Visual Studio 2005和2008中的配置文件引导优化通过插入对最可能的目标函数的直接调用来进行虚拟调用推测,内联电话,所以重量可能是相同的。