C#:虚拟函数调用甚至比委托调用更快?
我刚刚遇到一个代码设计问题。 说,我有一个“模板”方法,可以调用一些可能“改变”的函数。 直观的设计是遵循“模板设计模式”。 将更改函数定义为要在子类中重写的“虚拟”函数。 或者,我可以使用没有“虚拟”的委托function。 注入委托函数,以便它们也可以自定义。
最初,我认为第二种“委托”方式比“虚拟”方式更快,但是一些编码片段certificate它不正确。
在下面的代码中,第一个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 String.IsNullOrEmpty(str), null); sw.Stop(); Console.WriteLine(sw.ElapsedMilliseconds); } } }
想想每种情况下的要求:
虚拟电话
- 检查无效
- 从对象指针导航到类型指针
- 在指令表中查找方法地址
- (不确定 – 甚至里希特也不介绍这个)如果没有覆盖方法,请转到基类型? 递归,直到我们找到正确的方法地址。 (我不这么认为 – 请参见底部的编辑。)
- 将原始对象指针推入堆栈(“this”)
- 通话方式
代表电话
- 检查无效
- 从对象指针导航到调用数组(所有代理都可能是多播的)
- 循环遍历数组,并为每次调用:
- 获取方法地址
- 确定是否将目标作为第一个参数传递
- 将参数推送到堆栈(可能已经完成 – 不确定)
- (可选)(取决于调用是打开还是关闭)将调用目标推送到堆栈
- 通话方式
可能会有一些优化,因此单呼叫情况不会涉及循环,但即便这样也需要非常快速的检查。
但基本上,代表所涉及的间接性也是如此。 鉴于我在虚拟方法调用中不确定的位置,在大型深层类型层次结构中调用未重载的虚拟方法可能会更慢……我将尝试使用答案进行编辑。
编辑:我已尝试使用inheritance层次结构深度(最多20个级别),“最多派生重写”和声明的变量类型 – 并且它们中的任何一个似乎都没有区别。
编辑:我刚刚尝试使用接口(传入)的原始程序 – 最终具有与委托相同的性能。
只是想为约翰双向飞碟的回应添加一些修正:
虚方法调用不需要执行空检查(使用硬件陷阱自动处理)。
它也不需要走inheritance链来查找非重写方法(这就是虚方法表的用途)。
在调用时,虚方法调用本质上是一个额外的间接级别。 由于表查找和后续函数指针调用,它比普通调用慢。
委托调用还涉及额外的间接级别。
除非您使用DynamicInvoke方法执行动态调用,否则对代理的调用不涉及将参数放入数组中。
委托调用涉及调用方法,在有问题的委托类型上调用编译器生成的Invoke方法。 对谓词(值)的调用变为predicator.Invoke(value)。
反过来,Invoke方法由JIT实现,以调用函数指针(存储在委托对象内部)。
在您的示例中,您传递的委托应该已经实现为编译器生成的静态方法,因为实现不访问任何实例变量或本地,因此从堆中访问“this”指针的需要应该不是问题。
委托和虚函数调用之间的性能差异应该大致相同,并且您的性能测试表明它们非常接近。
差异可能是由于多播需要额外的检查+分支(如John所建议的)。 另一个原因可能是JIT编译器没有内联Delegate.Invoke方法,并且Delegate.Invoke的实现不执行参数以及执行虚方法调用时的实现。
虚拟调用在内存中的已知偏移量处取消引用两个指针。 它实际上不是动态绑定; 在运行时没有代码反映元数据以发现正确的方法。 编译器根据this指针生成几条指令来执行调用。 实际上,虚拟调用是单个IL指令。
谓词调用正在创建一个匿名类来封装谓词。 必须实例化该类,并且生成一些代码以实际检查谓词函数指针是否为空。
我建议你看看两者的IL结构。 只需调用两个DoSomthing中的每一个,即可编译上面源代码的简化版本。 然后使用ILDASM查看每个模式的实际代码。
(我相信我会因为没有使用正确的术语而被投票:-))
测试结果价值1000字: http : //kennethxu.blogspot.com/2009/05/strong-typed-high-performance_15.html
有可能因为您没有任何方法可以覆盖JIT能够识别此虚拟方法并使用直接调用。
对于类似这样的事情,通常最好像你一样测试它,而不是试着猜测性能是什么。 如果你想进一步了解委托调用是如何工作的,我建议杰弗里里希特出版的优秀书籍“CLR Via C#”。
我怀疑它能解释你所有的不同之处,但有一点可以解释一些不同之处就是虚拟方法调度已经准备就绪了。 通过委托调用时,必须从委托中获取this
指针。
请注意,根据这篇博客文章 ,.NET v1.x中的差异更大。
虚拟覆盖具有某种重定向表或者在编译时进行硬编码并完全优化的东西。 它非常快速。
代表是动态的,总是有一个开销,它们似乎也是对象,所以加起来。
您不应该担心这些小的性能差异(除非为军方开发性能关键软件),对于大多数目的而言,良好的代码结构优于优化。