C#:虚函数调用甚至比委托调用更快?

时间:2008-10-19 04:28:09

标签: c# performance delegates virtual

我刚刚遇到一个代码设计问题。说,我有一个“模板”方法,可以调用一些可能“改变”的函数。直观的设计是遵循“模板设计模式”。将更改函数定义为要在子类中重写的“虚拟”函数。或者,我可以使用没有“虚拟”的委托功能。注入委托函数,以便它们也可以自定义。

最初,我认为第二种“委托”方式比“虚拟”方式更快,但是一些编码片段证明它不正确。

在下面的代码中,第一个DoSomething方法遵循“模板模式”。它调用虚方法IsTokenChar。第二个DoSomthing方法不依赖于虚函数。相反,它有一个传入代理。在我的电脑中,第一个DoSomthing总是比第二个快。结果如1645:1780。

“虚拟调用”是动态绑定,应该比直接委托调用更耗时,对吗?但结果表明它不是。

任何人都可以解释一下吗?

using System;
using System.Diagnostics;

class Foo
{
    public virtual bool IsTokenChar(string word)
    {
        return String.IsNullOrEmpty(word);
    }

    // this is a template method
    public int DoSomething(string word)
    {
        int trueCount = 0;
        for (int i = 0; i < repeat; ++i)
        {
            if (IsTokenChar(word))
            {
                ++trueCount;
            }
        }
        return trueCount;
    }

    public int DoSomething(Predicate<string> predicator, string word)
    {
        int trueCount = 0;
        for (int i = 0; i < repeat; ++i)
        {
            if (predicator(word))
            {
                ++trueCount;
            }
        }
        return trueCount;
    }

    private int repeat = 200000000;
}

class Program
{
    static void Main(string[] args)
    {
        Foo f = new Foo();

        {
            Stopwatch sw = Stopwatch.StartNew();
            f.DoSomething(null);
            sw.Stop();
            Console.WriteLine(sw.ElapsedMilliseconds);
        }

        {
            Stopwatch sw = Stopwatch.StartNew();
            f.DoSomething(str => String.IsNullOrEmpty(str), null);
            sw.Stop();
            Console.WriteLine(sw.ElapsedMilliseconds);
        }
    }
}

7 个答案:

答案 0 :(得分:19)

考虑每种情况下的要求:

虚拟通话

  • 检查无效
  • 从对象指针导航到类型指针
  • 在指令表中查找方法地址
  • (不确定 - 甚至里希特也不介绍这个)如果没有覆盖方法,请转到基类型?递归,直到我们找到正确的方法地址。 (我不这么认为 - 请参见底部的编辑。)
  • 将原始对象指针推入堆栈(“this”)
  • 致电方法

委派电话

  • 检查无效
  • 从对象指针导航到调用数组(所有代理都可能是多播)
  • 循环数组,并为每次调用:
    • 获取方法地址
    • 确定是否将目标作为第一个参数传递
    • 将参数推送到堆栈上(可能已经完成 - 不确定)
    • (可选)(取决于调用是打开还是关闭)将调用目标推送到堆栈
    • 致电方法

可能会有一些优化,因此单一呼叫案例中不会涉及循环,但即便如此,也需要非常快速的检查。

但基本上,代表所涉及的间接性也是如此。鉴于我在虚拟方法调用中不确定的位置,在大型深层次类型层次结构中调用未重载的虚拟方法可能会更慢......我将尝试使用答案进行编辑。 / p>

编辑:我已经尝试过继承层次结构的深度(最多20个级别),“最多派生的重写”和声明的变量类型 - 它们似乎没有任何区别。 / p>

编辑:我刚刚尝试使用接口(传入)的原始程序 - 最终具有与委托大致相同的性能。

答案 1 :(得分:12)

只想为约翰双向飞碟的回应添加一些更正:

虚方法调用不需要进行空检查(使用硬件陷阱自动处理)。

它也不需要走继承链来查找非重写方法(这就是虚方法表的用途)。

虚拟方法调用在调用时基本上是一个额外的间接级别。由于表查找和后续函数指针调用,它比普通调用慢。

委托调用还涉及额外的间接级别。

对委托的调用不涉及将参数放在数组中,除非您使用DynamicInvoke方法执行动态调用。

委托调用涉及调用方法,在有问题的委托类型上调用编译器生成的Invoke方法。对谓词(值)的调用变为predicator.Invoke(value)。

Invoke方法依次由JIT实现,以调用函数指针(存储在委托对象内部)。

在您的示例中,您传递的委托应该已经实现为编译器生成的静态方法,因为实现不访问任何实例变量或本地,因此从堆中访问“this”指针的需要不应该是问题。

委托和虚函数调用之间的性能差异应该大致相同,并且您的性能测试表明它们非常接近。

差异可能是由于多播需要额外的检查+分支(如John所建议的)。另一个原因可能是JIT编译器没有内联Delegate.Invoke方法,并且Delegate.Invoke的实现不执行参数以及执行虚方法调用时的实现。

答案 2 :(得分:8)

虚拟调用在内存中众所周知的偏移量处取消引用两个指针。它实际上不是动态绑定;在运行时没有代码反映元数据以发现正确的方法。编译器根据this指针生成几条指令来执行调用。实际上,虚拟调用是一条IL指令。

谓词调用正在创建一个匿名类来封装谓词。必须实例化该类,并且生成一些代码以实际检查谓词函数指针是否为空。

我建议你看看两者的IL结构。只需调用两个DoSomthing中的每一个,即可编译上面源代码的简化版本。然后使用ILDASM查看每个模式的实际代码。

(我相信我会因为没有使用正确的术语而被投票: - )

答案 3 :(得分:3)

答案 4 :(得分:1)

由于您没有任何方法可以覆盖JIT能够识别的虚拟方法并使用直接调用,因此可能会这样做。

对于类似这样的事情,通常最好像你所做的那样测试它,而不是试着猜测它的性能。如果你想进一步了解委托调用是如何工作的,我建议杰弗里里希特出版的优秀书籍“CLR Via C#”。

答案 5 :(得分:1)

我怀疑它能解释你所有的不同之处,但有一点可以解释一些不同之处就是虚拟方法调度已经准备好了this指针。通过委托调用时,必须从委托中获取this指针。

请注意,根据this blog article,.NET v1.x中的差异甚至更大。

答案 6 :(得分:-1)

虚拟覆盖有某种重定向表或者在编译时进行硬编码并完全优化的东西。它非常快速。

代理是动态的,总是有开销,它们似乎也是对象,所以加起来。

你不应该担心这些小的性能差异(除非为军方开发性能关键软件),对于大多数用途来说,良好的代码结构胜过优化。