【发布时间】:2017-07-17 04:48:15
【问题描述】:
This benchmark 似乎表明直接在对象引用上调用虚方法比在此对象实现的接口的引用上调用它更快。
换句话说:
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# 是如何实现虚拟调用的?通过接口调用时显然要完成的“额外”工作是什么?
--- 编辑 ---
好的,到目前为止我得到的答案/cmets 暗示通过接口的虚拟调用有一个双指针取消引用,而通过对象的虚拟调用只有一个取消引用。
那么请有人解释一下为什么这是必要的吗? C#中虚拟表的结构是什么?它是“扁平的”(对于 C++ 来说很典型)吗?导致这种情况的 C# 语言设计中的设计权衡是什么?我并不是说这是一个“糟糕”的设计,我只是好奇为什么它是必要的。
简而言之,我想了解我的工具在后台做了什么,以便我可以更有效地使用它。如果我不再得到“你不应该知道”或“使用另一种语言”类型的答案,我将不胜感激。
--- 编辑 2 ---
为了清楚起见,我们不是在处理一些消除动态调度的 JIT 优化编译器:我修改了原始问题中提到的基准,以在运行时随机实例化一个类或另一个类。由于实例化发生在编译之后和程序集加载/JITing 之后,因此在这两种情况下都无法避免动态调度:
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
...表明指向同一对象内不同接口的指针实际上指向该对象的不同部分(即它们包含不同的物理地址)。
【问题讨论】:
-
C++ 不一定强制进行虚拟查找。如果可以在编译时确定动态类型,则可以直接调用正确的函数。
-
接口方法调用需要双指针解引用。如果您计算纳秒,C# 可能不应该是您选择的语言。 C 和 C++ 是为此优化的语言。
-
@Hans,我提出这个问题并不意味着我在任何具体项目上都“计算纳秒”。我就不能好奇吗?
-
你的问题没有很好地表达这种兴趣。
-
@Jeremy “简单”调用的性能降低了约 60%,这在大多数情况下会被性能的其他方面所淹没,我同意。但是,我不同意它在所有情况下都是微不足道的,所以我认为有眼光的编码人员应该意识到这一点。
标签: c# .net performance language-design