【问题标题】:C#: Virtual Function invocation is even faster than a delegate invocation?C#:虚函数调用比委托调用更快?
【发布时间】:2010-09-17 23:27:29
【问题描述】:

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

本来我以为第二种“委托”方式会比“虚拟”方式快,但是一些编码sn-p证明是不正确的。

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

【问题讨论】:

  • 一个给 Jon Skeet,我觉得! ;)
  • @Mitch:在回答之前我实际上并没有看到你的评论,但我很受宠若惊:)
  • 顺便说一句,我发现优化的构建更能体现差异

标签: c# performance delegates virtual


【解决方案1】:

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

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

【讨论】:

  • 不仅如此,如果存储/调用委托比虚拟调度更有效,框架几乎肯定会存储和调用委托而不是使用虚拟调度。鉴于它没有,它可能不是(在罕见/病理情况下除外)。
  • 我认为方法在父类中是“虚拟”或在子类中是“覆盖”并没有什么不同。如果是“虚拟”,则始终是动态绑定。
【解决方案2】:

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

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

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

【讨论】:

  • 快速更正:我认为您的意思是“虚拟覆盖”而不是“虚拟重载”
【解决方案3】:

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

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

【讨论】:

    【解决方案4】:

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

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

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

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

    【讨论】:

    • 在我的理解中,“实际调用的方法是在运行时决定的”称为“动态绑定”。
    • 谓词只是一个委托实例。这是一个实例,但我不太确定是否创建了一个“匿名类”。 C#/.NET 中是否有“匿名类”的概念?
    • 虚拟调用不是“决定在运行时调用哪个方法”。要调用的方法是众所周知的,并且已经与该类相关联。
    • 当我说“匿名类”时,我指的是编译器生成的用于包装委托调用的辅助类。 LINQ 引入了匿名类的单独概念。
    • 这个答案包含一个根本缺陷:C# 编译器生成的 IL 代码不会告诉您代码运行的速度; JIT 输出的汇编代码将是衡量执行速度的更可靠方法。这是因为 (1) IL 基于一个抽象堆栈机器,它很可能与底层(通常基于寄存器的)计算机体系结构不同,因此 IL 在执行之前必须进行相当大的转换;并且因为 (2) 优化显然主要是在 IL 之后而不是在 IL 之前执行(即由 JIT,而不是由 C# 编译器)。
    【解决方案5】:

    想想每种情况需要什么:

    虚拟通话

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

    代表电话

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

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

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

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

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

    【讨论】:

    • 在虚拟呼叫中没有检查是否为空。此外,方法的 vtable 是在编译时确定的,因此在基类上没有运行时递归。编译器从正确的基类生成指向正确方法的指针,并将其放入 vtable 的正确槽中。
    • callvirt 确实检查 null - 请参阅 CLI 规范中分区 3 的第 4.2 节,或通过 C# 的 CLR 的 P166。 (如果引用为空,将调用哪个实现?)感谢您确认“无递归”位。这就是实验的基本建议。
    • +1 表示无递归,vtables 在编译类型时被展平。
    【解决方案6】:

    只是想对 john skeet 的回复进行一些更正:

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

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

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

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

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

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

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

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

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

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

    【讨论】:

      【解决方案7】:
      猜你喜欢
      • 1970-01-01
      • 2011-04-23
      • 1970-01-01
      • 1970-01-01
      • 2019-09-20
      • 2021-05-31
      • 2019-11-11
      • 2013-01-18
      相关资源
      最近更新 更多