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

Joh*_*ell 8 c# c++ polymorphism virtual

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

Ian*_*ose 8

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

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

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

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

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


Pet*_*ham 5

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

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

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

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


Dan*_*ker 5

原问题说:

我似乎记得在某处读到过,C# 中的虚拟调用的成本相对而言没有C++ 高。

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

我似乎记得在 C# 中读到过,虚拟和非虚拟调用同样慢,而在 C++ 中,虚拟调用比非虚拟调用慢......

所以提问者并没有声称 C# 在任何情况下都比 C++ 快。

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

使用非虚拟方法:

struct Plain
{
    void Bar() { System::Console::WriteLine("hi"); }
};
Run Code Online (Sandbox Code Playgroud)

这段代码:

Plain *p = new Plain();
p->Bar();
Run Code Online (Sandbox Code Playgroud)

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

call void <Module>::Plain.Bar(valuetype Plain*)
Run Code Online (Sandbox Code Playgroud)

与继承层次结构进行比较:

struct Base
{
    virtual void Bar() = 0;
};

struct Derived : Base
{
    void Bar() { System::Console::WriteLine("hi"); }
};
Run Code Online (Sandbox Code Playgroud)

现在,如果我们这样做:

Base *b = new Derived();
b->Bar();
Run Code Online (Sandbox Code Playgroud)

calli相反,它会发出操作码,它会跳转到一个计算出的地址——所以在调用之前有很多 IL。通过将其转回 C#,我们可以看到发生了什么:

**(*((int*) b))(b);
Run Code Online (Sandbox Code Playgroud)

换句话说,将 的地址强制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();
Run Code Online (Sandbox Code Playgroud)

这将生成callvirt操作码,与在 C# 中完全一样:

callvirt instance void Base::Bar()
Run Code Online (Sandbox Code Playgroud)

因此,当针对 CLR 进行编译时,Microsoft 当前的 C++ 编译器在使用每种语言的标准功能时没有与 C# 相同的优化可能性;对于标准的 C++ 类层次结构,C++ 编译器生成包含用于遍历 vtable 的硬编码逻辑的代码,而对于 ref 类,它将它留给 JIT 来找出最佳实现。