8

我似乎记得在某处读到过,相对而言,C# 中的虚拟调用的成本不如 C++ 中的高。这是真的?如果是这样 - 为什么?

4

9 回答 9

8

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

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

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

C# 代码使用更多指令这一事实当然会减少可以放入缓存的代码量,其效果无法预测。

(如果 C++ 类使用多重继承,则成本会更高,因为必须修补“this”指针。同样,C# 中的接口会增加另一层重定向。)

于 2009-03-24T12:11:51.107 回答
5

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

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

在限制情况下,只有一个虚拟调用的实现并且调用的主体足够小,虚拟调用被简化为纯粹的内联代码。该技术用于Self 语言运行时,JVM 是从该运行时演变而来的。

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

于 2009-03-24T11:41:47.183 回答
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"); }
};

这段代码:

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

This generates the callvirt opcode, exactly as it would in C#:

callvirt instance void Base::Bar()

So when compiling to target the CLR, Microsoft's current C++ compiler doesn't have the same possibilities for optimization as C# does when using the standard features of each language; for a standard C++ class hierarchy, the C++ compiler generates code that contains hard-coded logic for traversing the vtable, whereas for a ref class it leaves it to the JIT to figure out the optimal implementation.

于 2009-03-24T13:58:18.197 回答
3

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

但这本质上是理论上的,我不会打赌!

于 2009-03-24T11:07:21.867 回答
2

C++ 中虚拟调用的成本是通过指针 (vtbl) 调用函数的成本。我怀疑 C# 能否更快地做到这一点,并且仍然能够在运行时确定对象类型......

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

于 2009-03-24T11:06:14.850 回答
1

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

于 2009-03-24T11:16:20.317 回答
0

在 C# 中,可以通过分析代码将虚函数转换为非虚函数。在实践中,它不会经常发生,足以产生很大的不同。

于 2009-03-24T13:04:06.737 回答
0

C# 扁平化了 vtable 和内联祖先调用,因此您不必链接继承层次结构来解决任何问题。

于 2009-03-24T13:19:15.110 回答
0

这可能不完全是您问题的答案,但是尽管 .NET JIT 优化了虚拟调用,正如大家之前所说的那样,Visual Studio 2005 和 2008 中的配置文件引导优化通过插入对最可能的目标函数的直接调用来进行虚拟调用推测,内联调用,因此权重可能相同。

于 2009-03-24T13:56:51.023 回答