这个基准测试似乎表明,在对象引用上直接调用虚方法比在此对象实现的接口引用上调用虚方法要快。
换句话说:
interface IFoo {
void Bar();
}
class Foo : IFoo {
public virtual void Bar() {}
}
void Benchmark() {
Foo f = new Foo();
IFoo f2 = f;
f.Bar(); // This is faster.
f2.Bar();
}
从C++世界来看,我本以为这两个调用应该是一样的(作为简单的虚拟表查找),并且具有相同的性能。C#如何实现虚拟调用,以及通过接口调用时明显会进行的“额外”工作是什么?--- 编辑 ---
好的,到目前为止我得到的回答/评论暗示通过接口的虚拟调用需要双重指针解引用,而通过对象的虚拟调用只需要一次解引用。
那么,有谁能解释一下为什么需要这样做吗?C#中虚拟表的结构是什么样的?它是“平”的(与C++典型的方式相同)还是不同的?在C#语言设计方面做出了哪些权衡,导致了这种情况?我并不是说这是一个“糟糕”的设计,我只是好奇为什么需要这样做。
简而言之,我想了解我的工具在幕后是如何工作的,以便更有效地使用它。如果不再给我任何“你不应该知道那个”的答案或“使用其他语言”的类型,我将不胜感激。
--- 编辑2 ---
只是为了让清楚,我们没有处理某些编译器或JIT优化,从原始问题中提到的基准测试进行了修改,以在运行时随机实例化一个类或另一个类。由于实例化发生在编译和装配加载/JIT之后,因此无法避免两种情况下的动态调度:
interface IFoo {
void Bar();
}
class Foo : IFoo {
public virtual void Bar() {
}
}
class Foo2 : Foo {
public override void Bar() {
}
}
class Program {
static Foo GetFoo() {
if ((new Random()).Next(2) % 2 == 0)
return new Foo();
return new Foo2();
}
static void Main(string[] args) {
var f = GetFoo();
IFoo f2 = f;
Console.WriteLine(f.GetType());
// JIT warm-up
f.Bar();
f2.Bar();
int N = 10000000;
Stopwatch sw = new Stopwatch();
sw.Start();
for (int i = 0; i < N; i++) {
f.Bar();
}
sw.Stop();
Console.WriteLine("Direct call: {0:F2}", sw.Elapsed.TotalMilliseconds);
sw.Reset();
sw.Start();
for (int i = 0; i < N; i++) {
f2.Bar();
}
sw.Stop();
Console.WriteLine("Through interface: {0:F2}", sw.Elapsed.TotalMilliseconds);
// Results:
// Direct call: 24.19
// Through interface: 40.18
}
}
--- 编辑3 ---
如果有人感兴趣,这是我使用Visual C++ 2010布局多重继承类的实例的方法:
代码:
class IA {
public:
virtual void a() = 0;
};
class IB {
public:
virtual void b() = 0;
};
class C : public IA, public IB {
public:
virtual void a() override {
std::cout << "a" << std::endl;
}
virtual void b() override {
std::cout << "b" << std::endl;
}
};
调试器:
c {...} C
IA {...} IA
__vfptr 0x00157754 const C::`vftable'{for `IA'} *
[0] 0x00151163 C::a(void) *
IB {...} IB
__vfptr 0x00157748 const C::`vftable'{for `IB'} *
[0] 0x0015121c C::b(void) *
多个虚表指针明显可见,sizeof(C) == 8
(在32位构建中)。
...
C c;
std::cout << static_cast<IA*>(&c) << std::endl;
std::cout << static_cast<IB*>(&c) << std::endl;
..打印输出...
0027F778
0027F77C
这表明指向同一对象内不同接口的指针实际上指向该对象的不同部分(即它们包含不同的物理地址)。