我刚刚遇到一个代码设计问题.说,我有一个"模板"方法,可以调用一些可能"改变"的函数.直观的设计是遵循"模板设计模式".将更改函数定义为要在子类中重写的"虚拟"函数.或者,我可以使用没有"虚拟"的委托功能.注入委托函数,以便它们也可以自定义.
最初,我认为第二种"委托"方式比"虚拟"方式更快,但是一些编码片段证明它不正确.
在下面的代码中,第一个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(Predicatepredicator, 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个级别),"最多派生重写"和声明的变量类型 - 并且它们中没有一个似乎有所作为.
编辑:我刚刚尝试使用接口(传入)的原始程序 - 最终具有与委托相同的性能.
想想每种情况下的要求:
虚拟电话
检查无效
从对象指针导航到类型指针
在指令表中查找方法地址
(不确定 - 甚至里希特也不介绍这个)如果没有覆盖方法,请转到基类型?递归,直到我们找到正确的方法地址.(我不这么认为 - 请参见底部的编辑.)
将原始对象指针推入堆栈("this")
通话方式
代表电话
检查无效
从对象指针导航到调用数组(所有委托都可能是多播的)
循环数组,并为每次调用:
获取方法地址
确定是否将目标作为第一个参数传递
将参数推送到堆栈(可能已经完成 - 不确定)
(可选)(取决于调用是打开还是关闭)将调用目标推送到堆栈
通话方式
可能会有一些优化,因此单呼叫情况不会涉及循环,但即使这样也需要非常快速的检查.
但基本上,代表所涉及的间接性也是如此.鉴于我在虚拟方法调用中不确定的位置,在大规模深层次类型层次结构中调用未重载的虚拟方法可能会更慢......我将试一试并使用答案进行编辑.
编辑:我已尝试使用继承层次结构深度(最多20个级别),"最多派生重写"和声明的变量类型 - 并且它们中没有一个似乎有所作为.
编辑:我刚刚尝试使用接口(传入)的原始程序 - 最终具有与委托相同的性能.
只是想为约翰双向飞碟的回应添加一些修正:
虚方法调用不需要执行空检查(使用硬件陷阱自动处理).
它也不需要走向继承链来查找非重写方法(这就是虚方法表的用途).
在调用时,虚方法调用本质上是一个额外的间接级别.由于表查找和后续函数指针调用,它比普通调用慢.
委托调用还涉及额外的间接级别.
除非您使用DynamicInvoke方法执行动态调用,否则对委托的调用不涉及将参数放入数组中.
委托调用涉及调用方法,在有问题的委托类型上调用编译器生成的Invoke方法.对谓词(值)的调用变为predicator.Invoke(value).
反过来,Invoke方法由JIT实现,以调用函数指针(存储在委托对象内部).
在您的示例中,您传递的委托应该已经实现为编译器生成的静态方法,因为实现不访问任何实例变量或本地,因此从堆中访问"this"指针的需要应该不是问题.
委托和虚函数调用之间的性能差异应该大致相同,并且您的性能测试表明它们非常接近.
差异可能是由于多播需要额外的检查+分支(如John所建议的).另一个原因可能是JIT编译器没有内联Delegate.Invoke方法,并且Delegate.Invoke的实现不执行参数以及执行虚方法调用时的实现.
虚拟调用在内存中的已知偏移量处取消引用两个指针.它实际上不是动态绑定; 在运行时没有代码反映元数据以发现正确的方法.编译器根据this指针生成几条指令来执行调用.实际上,虚拟调用是单个IL指令.
谓词调用正在创建一个匿名类来封装谓词.必须实例化该类,并且生成一些代码以实际检查谓词函数指针是否为空.
我建议你看看两者的IL结构.只需调用两个DoSomthing中的每一个,即可编译上面源代码的简化版本.然后使用ILDASM查看每个模式的实际代码.
(我相信我会因为没有使用正确的术语而被投票:-))