我记得在某处读到过,在C#中虚拟调用的成本相对较低,而在C++中则不然。这是真的吗?如果是,为什么呢?
我记得在某处读到过,在C#中虚拟调用的成本相对较低,而在C++中则不然。这是真的吗?如果是,为什么呢?
然而,我们最多只谈论一小部分处理器指令。在现代超标量处理器上,很可能“null check”指令与“call method”同时运行,因此不需要时间。
如果在循环中进行调用,则所有处理器指令很可能已经在一级缓存中。但是数据不太可能被缓存,这些天从主存读取数据值的成本与从一级缓存中运行数百条指令的成本相同。因此,在实际应用程序中,虚拟调用的成本甚至在非常少的地方也无法测量。
当然,C#代码使用更多指令将减少可以适合缓存的代码量,这种影响是不可预测的。
(如果C++类使用多重继承,则成本更高,因为必须修补“this”指针。同样,在C#中,接口添加了另一层重定向。)
struct Plain
{
void Bar() { System::Console::WriteLine("hi"); }
};
Plain *p = new Plain();
p->Bar();
...会导致call
操作码被发出,传递特定的方法名称,并将隐式this
参数传递给Bar。
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
参数传递给它。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(公共语言运行时)为目标时,微软当前的C++编译器在优化方面与C#使用各自标准特性时并没有相同的可能性。对于一个标准的C++类层次结构,C++编译器会生成包含硬编码逐个遍历vtable的逻辑的代码,而对于一个ref类,则由JIT来找出最佳实现。
我猜这个假设是基于JIT编译器的,意味着C#可能会在实际使用之前将虚拟调用转换为简单的方法调用。
但这基本上是理论性的,我不会打赌!
C++中虚函数调用的成本是通过指针(vtbl)进行函数调用的成本。我怀疑C#能否更快地执行此操作,并且仍然能够在运行时确定对象类型...
编辑:正如Pete Kirkham所指出的那样,一个好的JIT可能能够内联C#调用,避免管道停顿;这是大多数C++编译器目前无法做到的。另一方面,Ian Ringrose提到了对缓存使用的影响。除非在实际工作负载下在目标机器上进行了性能分析,否则我个人不会真的介意。这只是微小的优化。
关于完整框架我不确定,但在紧凑框架中,它会更慢,因为CF没有虚拟调用表,尽管它会缓存结果。这意味着,在CF中进行虚拟调用时,第一次调用会更慢,因为它必须进行手动查找。如果应用程序内存不足,则每次调用都可能很慢,因为缓存的查找可能被清除。
C#会将虚函数表展开并内联祖先调用,因此您不需要在继承层次结构中链接任何内容来解决问题。
这可能并不完全是你问题的答案,但是尽管 .NET JIT 像大家之前说的那样优化了虚拟调用,但是 Visual Studio 2005 和 2008 中的分析指导优化通过插入一个直接调用到最有可能的目标函数来进行虚拟调用猜测,将调用内联,这样权重可能是相同的。