17

它只是发生在我的一个代码设计问题上。比如说,我有一个“模板”方法,它调用一些可能“改变”的函数。一个直观的设计是遵循“模板设计模式”。将更改函数定义为要在子类中覆盖的“虚拟”函数。或者,我可以只使用没有“虚拟”的委托函数。委托函数被注入,因此它们也可以被定制。

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

在下面的代码中,第一个 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);
        }
    }
}
4

7 回答 7

21

想想每种情况下需要什么:

虚拟通话

  • 检查无效性
  • 从对象指针导航到类型指针
  • 在指令表中查找方法地址
  • (不确定 - 即使 Richter 也没有涵盖这一点)如果方法没有被覆盖,则转到基本类型?递归直到我们找到正确的方法地址。(我不这么认为 - 请参阅底部的编辑。)
  • 将原始对象指针压入堆栈(“this”)
  • 调用方法

代表电话

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

可能会进行一些优化,以便在单次调用情况下不涉及循环,但即使这样也需要非常快速的检查。

但基本上,委托也涉及到同样多的间接性。鉴于我在虚拟方法调用中不确定的一点,在非常深的类型层次结构中调用未覆盖的虚拟方法可能会更慢......我会试一试并编辑答案。

编辑:我尝试过使用继承层次结构的深度(最多 20 级)、“最派生的覆盖”点和声明的变量类型——它们似乎都没有什么不同。

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

于 2008-10-19T06:49:29.913 回答
12

只是想对 john skeet 的回复添加一些更正:

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

它也不需要遍历继承链来查找非覆盖方法(这就是虚拟方法表的用途)。

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

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

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

委托调用涉及调用方法对相关委托类型调用编译器生成的 Invoke 方法。对 predicator(value) 的调用被转换为 predicator.Invoke(value)。

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

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

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

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

于 2009-02-06T00:43:51.080 回答
8

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

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

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

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

于 2008-10-19T05:57:50.220 回答
3

测试结果值 1000 字: http: //kennethxu.blogspot.com/2009/05/strong-typed-high-performance_15.html

于 2009-06-26T01:29:22.047 回答
1

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

对于这样的事情,通常最好像您所做的那样对其进行测试,而不是尝试猜测性能会是什么。如果您想了解有关委托调用如何工作的更多信息,我建议您阅读 Jeffrey Richter 的优秀书籍“CLR Via C#”。

于 2008-10-19T04:40:34.243 回答
1

我怀疑它解释了你的所有差异,但我脑海中浮现的一件事可能会解释一些差异,那就是虚拟方法调度已经this准备好指针了。通过委托调用时this,必须从委托中获取指针。

请注意,根据这篇博客文章,.NET v1.x 中的差异甚至更大。

于 2008-10-19T05:27:40.950 回答
0

虚拟覆盖具有某种重定向表或在编译时经过硬编码和完全优化的东西。它是一成不变的,非常快。

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

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

于 2008-10-19T05:26:40.423 回答