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

时间:2009-03-24 10:59:50

标签: c# c++ polymorphism virtual

我似乎记得在某处读到C#中虚拟呼叫的成本并不像C ++那样高。这是真的?如果是这样 - 为什么?

9 个答案:

答案 0 :(得分:8)

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

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

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

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

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

答案 1 :(得分:5)

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

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

在极限情况下,只有一个虚拟呼叫的实现,并且呼叫的主体足够小,虚拟呼叫减少到纯inline code。这种技术用于Self language运行时,JVM是从它发展而来的。

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

答案 2 :(得分:4)

最初的问题是:

  

我似乎记得在某处读书   C#中虚拟呼叫的成本   没有那么高,相对而言   说话,就像在C ++中一样。

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

  

我似乎记得在某处读书   在C#中,虚拟和非虚拟   调用同样很慢,而在C ++中   虚拟呼叫比a慢   非虚拟电话......

因此,在任何情况下,提问者都没有声称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 <Module>::Plain.Bar(valuetype Plain*)

与继承层次结构比较:

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 ++编译器与使用每种语言的标准功能时C#所做的优化不同;对于标准C ++类层次结构,C ++编译器生成的代码包含用于遍历vtable的硬编码逻辑,而对于ref类,它将它留给JIT以确定最佳实现。

答案 3 :(得分:3)

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

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

答案 4 :(得分:2)

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

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

答案 5 :(得分:1)

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

答案 6 :(得分:0)

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

答案 7 :(得分:0)

C#展开vtable并内联祖先调用,这样你就不会链接继承层次结构来解决任何问题。

答案 8 :(得分:0)

这可能不是你问题的答案,但是尽管.NET JIT优化了之前所有人都说过的虚拟调用,但是Visual Studio 2005和2008中的profile-guided optimization通过直接调用来实现虚拟调用。最有可能是目标函数,内联呼叫,因此权重可能相同。