c# - "direct"虚拟调用与 C# 中的接口(interface)调用的性能

标签 c# .net performance language-design

This benchmark似乎表明直接在对象引用上调用虚拟方法比在对该对象实现的接口(interface)的引用上调用它更快。

换句话说:

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# 如何实现虚拟调用以及通过接口(interface)调用时明显完成的“额外”工作是什么?

--- 编辑---

好吧,到目前为止,我得到的答案/评论表明,对于通过接口(interface)的虚拟调用,存在双指针解引用,而对于通过对象的虚拟调用,只有一个解引用。

所以请有人解释一下为什么这是必要的? C#中虚表的结构是怎样的?它是否“扁平化”(C++ 的典型特征)?在导致这种情况的 C# 语言设计中做出的设计权衡是什么?我并不是说这是一个“糟糕”的设计,我只是想知道为什么有必要这样做。

简而言之,我想了解我的工具在幕后做了什么,以便我可以更有效地使用它。如果我不再得到“你不应该知道”或“使用另一种语言”类型的答案,我将不胜感激。

--- 编辑 2 ---

为了清楚起见,我们在这里不处理某些删除动态分派(dispatch)的 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

...表示指向同一对象内不同接口(interface)的指针实际上指向该对象的不同部分(即它们包含不同的物理地址)。

最佳答案

我认为文章 Drill Into .NET Framework Internals to See How the CLR Creates Runtime Objects 将回答您的问题。特别是,请参阅 * Interface Vtable Map and Interface Map 部分-,以及以下关于虚拟调度的部分。

JIT 编译器可能会为您的简单案例解决问题并优化代码。但一般情况下不会。

IFoo f2 = GetAFoo();

GetAFoo 被定义为返回一个 IFoo,那么 JIT 编译器将无法优化调用。

关于c# - "direct"虚拟调用与 C# 中的接口(interface)调用的性能,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/42471076/

相关文章:

c# - 在 WPF 中解析本地 XML 文档(调试工作,发布后失败)

c# - 如何在 xamarin ios 中将 NSArray 转换为 List<String>

C# 关联数组

.net - 多个应用程序共享相同的配置文件

c# - Entity Framework Code First AddOrUpdate 方法插入重复值

sql - 为什么 UDF 比子查询慢这么多?

c# - 如何在 Winform 应用程序中创建 Global.asax? - C#/.NET

c# - Silverlight 组合框空项目高度

performance - Tomcat 尖峰并消耗所有可用的 CPU

mysql - 慢内连接顺序查询