关于delegate的执行效率,我也是偶然间关注的一个问题。我对一个问题提出了自己的解决方案,只不过这个解决方案是使用代理的。同时其他人又提出了直接封装一个类,调用这个类中的封装的方法去解决这个问题。针对我们遇到的问题,我们都一致认为代理是解决我们问题比较合适的方式。但是,他们也提出代理的效率是非常低下的,如果我们使用代理,会影响我们系统的性能。于是我就想:代理的执行效率真的很低吗?
写个测试程序试试呗。
首先写一个简单的代理类,源码如下:
1 public class DelegateT 2 { 3 public delegate int del(int a); 4 private event del DelHandle; 5 public void Reg(del d) 6 { 7 DelHandle = d; 8 } 9 public void UnReg(del d) 10 { 11 DelHandle -= d; 12 } 13 14 public int DoSomething(int a) 15 { 16 //这里可以做一些事情。 17 18 if (DelHandle != null) 19 { 20 return DelHandle(a);//触发事件,调用注册的方法。 21 } 22 else 23 { 24 return 0; 25 } 26 } 27 }
可以看到,这个类是非常的简单。下面的测试程序也是非常简单的,对比了分别用三种不同的代理方式和一种直接调用方法的方式去调用一个函数,然后打印出这四种方式的执行时间。为了让执行的时间差别显示出来,我分别重复调用了1000万次。由于我们web程序,于是我就将测试程序放在WebForm里了。
用四种不同的方式去循环调用的方法源码:
1 private string f_1()//测试函数,注意下面定义了一个重载的被调函数。 2 { 3 int a = 10000000; 4 string strInfo = "测试程序:<br />"; 5 { 6 Stopwatch sw = Stopwatch.StartNew(); 7 DelegateT obj = new DelegateT(); 8 for (int i = 0; i < a; i++) 9 { 10 obj.Reg(f_1); 11 obj.DoSomething(i);//调用代理方式一 12 } 13 sw.Stop(); 14 strInfo += "方式一:" + sw.ElapsedMilliseconds + ";<br />"; 15 } 16 17 { 18 Func<int, int> del = f_1; 19 Stopwatch sw = Stopwatch.StartNew(); 20 for (int i = 0; i < a; i++) 21 { 22 del(i);//调用代理方式二 23 } 24 sw.Stop(); 25 strInfo += "方式二:" + sw.ElapsedMilliseconds + ";<br />"; 26 } 27 28 { 29 Stopwatch sw = Stopwatch.StartNew(); 30 for (int i = 0; i < a; i++) 31 { 32 new Func<int, int>(f_1)(a); //调用代理方式三,其实就是方式二的匿名代理。我们也比较一下差别吧,:)。 33 } 34 sw.Stop(); 35 strInfo += "方式三:" + sw.ElapsedMilliseconds + ";<br />"; 36 } 37 38 39 { 40 Stopwatch sw = Stopwatch.StartNew(); 41 for (int i = 0; i < a; i++) 42 { 43 f_1(i);//直接调用方法 44 } 45 sw.Stop(); 46 strInfo += "方式四:" + sw.ElapsedMilliseconds; 47 } 48 return strInfo; 49 }
被调用的方法:这个方法是一个和上面的方法重载的一个方法,定义如下:
private int f_1(int a)//被调函数 { //for (int i = 0; i < 10; i ++) //{ //} return a; }
这样,在WebForm的Page_Load函数中将f_1()返回的执行时间信息显示在页面上,即可非常清楚的看出四种不用的调用方法的执行效率。
代码如下:
1 protected void Page_Load(object sender, EventArgs e) 2 { 3 Response.Write(f_1()); 4 }
如果我们定义:函数执行的总的时间为Z,调用这个过程本身执行的时间X,被调函数执行的时间为Y。
那么Z=X+Y,页面上显示的就是四种不同的方式执行的总时间。
下面分情况对比讨论委托本身的执行效率。
情况一:当被调用函数不做任何处理,执行时间趋近于0,也就是 Y 趋近于0。那么函数执行的总时间 Z 就基本上代表了四种不同的调用方式本身的时间。五次测试结果如下:
调用方式 | 执行时间 | 执行时间 | 执行时间 | 执行时间 | 执行时间 |
方式一 | 538 | 581 | 616 | 556 | 600 |
方式二 | 148 | 144 | 144 | 145 | 145 |
方式三 | 286 | 353 | 368 | 319 | 357 |
方式四 | 145 | 140 | 141 | 141 | 139 |
可以看到方式一(通过委托)调用方法比方式四(直接)调用方法的效率确实低了4~5倍。这些时间都消耗在创建委托类,为委托类注册方法等一些必须的准备工作上面,所以效率会低一点。从方式二和方式三上也能看出来。方式二和方式三用的是用一种委托的调用方式,方式三只不过比方式二多创建了999,9999个匿名对象而已,所以效率就低了很多。同时也可以看出方式二的调用方式和方式四几乎是无差别的,所以我们可以大胆的采用方式二的方式调用方法。只不过方式二一定的局限性,只能有一个入参。
情况二:当被调函数进行一个空的for循环,先让它空循环10次,把被调函数的注释的代码还原,也就是:
1 private int f_1(int a) 2 { 3 for (int i = 0; i < 10; i ++) 4 { 5 6 } 7 return a; 8 }
。测试的结果如下:
调用方式 | 执行时间 | 执行时间 | 执行时间 | 执行时间 | 执行时间 |
方式一 | 841 | 802 | 826 | 876 | 791 |
方式二 | 399 | 398 | 397 | 406 | 397 |
方式三 | 568 | 564 | 591 | 613 | 559 |
方式四 | 398 | 396 | 397 | 396 | 397 |
情况三:当被调函数空循环100次。测试结果如下:
调用方式 | 执行时间 | 执行时间 | 执行时间 | 执行时间 | 执行时间 |
方式一 | 3106 | 3071 | 3134 | 3123 | 3163 |
方式二 | 2537 | 2540 | 2619 | 2554 | 2557 |
方式三 | 2755 | 2839 | 2960 | 2844 | 2885 |
方式四 | 2538 | 2537 | 2631 | 2571 | 2581 |
从情况三的表中可以看出我们进行1000万次的委托调用与直接进行1000万次的方法调用的时间大约相差了0.5~0.6秒。
通过情况一,二,三测试数据的比较,我们可以很容易的得出结论:如果说我们以直接调用方法(调用方式四)的效率是最高的,那么被调函数的逻辑越复杂,被调函数用的时间变长,也就是 在上面的代数式:Z=X+Y 中,Y 变大,委托本身消耗的时间(X),小到可以忽略不计。事实上,我们的被调函数往往不是简单的空循环100次,往往伴随着非常复杂的逻辑。然而,就算是我们的被调函数简单的执行100次空循环,从实验的结果可以看出我们进行了1000万次的调用,相差也就0.5~0.6秒,我觉得这委托事件的调用效率是可以接受的。
综上所述,委托确实不比直接调用方法快,但是也不会差劲到影响系统的性能。而且,我们在解决一些关联性关系问题的时候,用委托往往会很方便,也使代码很简洁,优雅。
使用方便的东西,往往会有负面效果。我们做程序的过程,就是一个权衡利弊的过程,最终给用户一个相对完美的产品。
这些测试不免有一些没有考虑到的地方,不足之处还行大神斧正。