昨天的文章《用JScript.net写.net应用程序》一文写了之后,对于其运行效率问题有了一点疑问,所以需要进行以下测试,前人当然做过很多种测试,不过bigtall的测试方法有些不同。这里我采用了斐波纳契的两个算法,一个是递归实现,一个是迭代实现。采用斐波纳契的理由如下:
- 它是一个复杂度为O(2n)的算法,计算量足够大
- 相同的计算,递归和迭代的主要区别是堆栈的处理,我们也可以同时比较一下不同语言在调用函数之间的效率差别。
- 代码简单,而且算法容易理解。不同测试代码之间的差别也小,不容易起争议。
测试运行时候考虑到如下的情况:
- 第一次系统装入是从磁盘装入,而后几次则是直接从磁盘缓存装入,所每个测试连续运行4遍,第一遍时间不计入。
- 因为IO库实现效率不同,所以算法代码中不存在任何IO调用,纯计算代码。
参与比较的语言包括c,c#,标准的javascript,JScript.net,后来觉得不过瘾,把java6也加上了。加上bigtall自己写的一个计算时间的小程序和批处理,一共12段代码,表示如下:
fibc.c | fib2c.c |
long Fib(long n) void main() | long Fib(long n) void main() |
fibcs.cs | fib2cs.cs |
public class A public static void Main() } | public class A public static void Main() } |
fibjava.java | fib2java.java |
public class fibjava public static void main(String[] args) } | public class fib2java public static void main(String[] args) } |
fibjs1.js | fib2js1.js |
function Fib(n) for(var i:int = 0; i < 10; i++) | function Fib(n) for(var i:int = 0; i < 26925370; i++) |
fibjs2.js | fib2js2.js |
function Fib(n:int):int for(var i:int = 0; i < 10; i++) | function Fib(n:int):int for(var i:int = 0; i < 26925370; i++) |
ptime.cs | cp.bat |
public class A } | @echo off csc /o+ /debug- fibcs.cs jsc /fast- /debug- fibjs1.js jsc /fast+ /debug- fibjs2.js "%JAVA_HOME%\bin\javac" -g:none fibjava.java echo 编译完成 call:run fibc goto end :end |
运行测试的结果如下表格所示,表格内部蓝色的4组数据分别为1,2,3,4测试数据,黑色数据为后三组测试结果的平均数,绿色数据为相对C语言运行耗时的比例,最后一行红色纵比数字为相同语言【单次】递归和迭代算法的耗时比例。时间单位为百分之一秒:
C | C# | java | js | JScript.net | |
递归 (10次) | 119 | 486 | 2258 | 75397 | 1571 |
48.67 | 447 | 483.67 | 75326.33 | 1500.67 | |
1 | 9.18 | 9.94 | 1547.70 | 30.83 | |
迭代 (26,925,370次) | 120 | 5261 | 7880 | 125786 | 9196 |
47.33 | 5040 | 7765.67 | 127310.33 | 9102.67 | |
1 | 106.48 | 164.06 | 2689.65 | 192.31 | |
纵比 | 0.97 | 11.28 | 16.06 | 1.69 | 6.07 |
由此,我们看出,如果横向比较,以C语言运行速度为标准,递归运算的时候,C#和java的速度都慢了将近10倍,JScript.net慢了将近31倍,js因为使用了运行时绑定,速度慢了1500倍之多;而一旦消除了函数调用,使用纯计算代码的迭代算法的运行时间上,各种语言相差更大,而且明显C#代码比java快(这里没有考虑基础类库装入的差别,因为M$对.net有预装入),最差的依然是javascript,不过看起来不带调用的后期绑定似乎更快一些。不过令人惊讶的是JScript.net的编译优化做的不错,速度算是很快了。
纵向比较之前,我们需要对算法进行一下分析,通过简单的代码,我们得知fib(30)的递归调用次数为2692537次,10次重复就是26925370次。这个就是递归和迭代算法的区别所在,但是我们把迭代的次数也设定为26925370,以消除函数调用的差别,突出代码的线性运行差别。通过对代码的分析,我们得出代码特征的统计表格:
递归 | 迭代 | |
赋值语句 | 3 | 120 |
变量分配 | 2 | 4 |
函数调用 | 2 | 0 |
返回 | 2 | 2 |
条件判断 | 1 | 30 |
跳转 | 1 | 30 |
累计 | 11 | 186 |
迭代算法和递归算法相比,明显代码量较大,其代码规模大约是递归的186/11=16.9倍。但是运行时间中除了java体现出了这一比例之外,其他都比这个比例要小。C语言甚至时间更短,如果不考虑测试误差,唯一合理的解释应该是代码优化问题,因为编译器和CPU都有优化代码的能力,但是显然无论是哪种优化,都无法跨越函数调用进行优化;C#比java要快,是不是说明C#的优化器比java要好一些呢?但是JS代码的两个比例值有点让我难住了,但是也并非不可解释,因为js代码中间可以优化的地方实在是太多太多了。
结论:
- C作为一种老牌的中高级语言,优势没得说。
- 递归少用,尤其是JavaScript,连函数调用都尽可能压缩一些。
- 本文的最主要目的,如果用JScript.net做一般应用程序,效率应该属于可以接受的范围,但是千万不要进行数值计算。
- 以前看过什么java或者C#运行效率可以达到C语言的70%之类的文章,现在看来是有水分的,如果单纯比较编译器的效率,我看差距还是明显的。看来枪手文章还是要警惕啊!
另外给各位看官提一个小小的请求,如果哪位对python,ruby,perl等熟悉的,用相同算法做一个测试如何?
本文章算法参考了浅议Fibonacci(斐波纳契)数列求解。
========================================
2007-11-16 17:30修改迭代部分的循环次数为26925370次,重新更正相关测试的时间和部分结论。非常感谢装配脑袋的提醒。谢谢!另外对之前给大家的误导表示歉意!