当前位置:  开发笔记 > 编程语言 > 正文

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

如何解决《C#:虚函数调用甚至比委托调用更快?》经验,为你挑选了3个好方法。

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

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

在下面的代码中,第一个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 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.. 20

想想每种情况下的要求:

虚拟电话

检查无效

从对象指针导航到类型指针

在指令表中查找方法地址

(不确定 - 甚至里希特也不介绍这个)如果没有覆盖方法,请转到基类型?递归,直到我们找到正确的方法地址.(我不这么认为 - 请参见底部的编辑.)

将原始对象指针推入堆栈("this")

通话方式

代表电话

检查无效

从对象指针导航到调用数组(所有委托都可能是多播的)

循环数组,并为每次调用:

获取方法地址

确定是否将目标作为第一个参数传递

将参数推送到堆栈(可能已经完成 - 不确定)

(可选)(取决于调用是打开还是关闭)将调用目标推送到堆栈

通话方式

可能会有一些优化,因此单呼叫情况不会涉及循环,但即使这样也需要非常快速的检查.

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

编辑:我已尝试使用继承层次结构深度(最多20个级别),"最多派生重写"和声明的变量类型 - 并且它们中没有一个似乎有所作为.

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



1> Jon Skeet..:

想想每种情况下的要求:

虚拟电话

检查无效

从对象指针导航到类型指针

在指令表中查找方法地址

(不确定 - 甚至里希特也不介绍这个)如果没有覆盖方法,请转到基类型?递归,直到我们找到正确的方法地址.(我不这么认为 - 请参见底部的编辑.)

将原始对象指针推入堆栈("this")

通话方式

代表电话

检查无效

从对象指针导航到调用数组(所有委托都可能是多播的)

循环数组,并为每次调用:

获取方法地址

确定是否将目标作为第一个参数传递

将参数推送到堆栈(可能已经完成 - 不确定)

(可选)(取决于调用是打开还是关闭)将调用目标推送到堆栈

通话方式

可能会有一些优化,因此单呼叫情况不会涉及循环,但即使这样也需要非常快速的检查.

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

编辑:我已尝试使用继承层次结构深度(最多20个级别),"最多派生重写"和声明的变量类型 - 并且它们中没有一个似乎有所作为.

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


callvirt*确实*检查为null - 请参阅CLI规范中的分区3的4.2节,或通过C#的CLR的P166.(如果引用为null,将调用哪个实现?)感谢您确认"无递归"位.这就是实验基本上建议的内容.
没有递归的+1,vtable在编译类型时被展平.

2> 小智..:

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

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

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

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

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

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

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

反过来,Invoke方法由JIT实现,以调用函数指针(存储在委托对象内部).

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

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

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



3> Franci Penov..:

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

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

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

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


这个答案包含一个基本缺陷:C#编译器生成的IL代码不会告诉你代码运行的速度有多快; JIT输出的汇编代码是衡量执行速度的更可靠方法.这是因为(1)IL基于抽象堆栈机器,它很可能与底层(通常是基于寄存器的)计算机体系结构不同,因此IL必须在执行之前进行相当大的转换.并且因为(2)优化显然主要在IL之后执行,而不是在IL之前执行(即通过JIT,而不是通过C#编译器).
推荐阅读
Chloemw
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有