老翅寒暑

一个老鸟的自白
随笔 - 75, 文章 - 0, 评论 - 658, 引用 - 23
数据加载中……

JScript.net的运行效率测试(兼多种语言效率对比)

昨天的文章《用JScript.net写.net应用程序》一文写了之后,对于其运行效率问题有了一点疑问,所以需要进行以下测试,前人当然做过很多种测试,不过bigtall的测试方法有些不同。这里我采用了斐波纳契的两个算法,一个是递归实现,一个是迭代实现。采用斐波纳契的理由如下:

  1. 它是一个复杂度为O(2n)的算法,计算量足够大
  2. 相同的计算,递归和迭代的主要区别是堆栈的处理,我们也可以同时比较一下不同语言在调用函数之间的效率差别。
  3. 代码简单,而且算法容易理解。不同测试代码之间的差别也小,不容易起争议。

测试运行时候考虑到如下的情况:

  1. 第一次系统装入是从磁盘装入,而后几次则是直接从磁盘缓存装入,所每个测试连续运行4遍,第一遍时间不计入。
  2. 因为IO库实现效率不同,所以算法代码中不存在任何IO调用,纯计算代码。

参与比较的语言包括c,c#,标准的javascript,JScript.net,后来觉得不过瘾,把java6也加上了。加上bigtall自己写的一个计算时间的小程序和批处理,一共12段代码,表示如下:

fibc.c fib2c.c

long Fib(long n)
{
    if (n <= 1) {
        return n;
    } else {
        return Fib(n - 1) + Fib(n - 2);
    }
}

void main()
{
    int i;
    for(i = 0; i < 10; i++)
        Fib(30);
}

long Fib(long n)
{
    int i;
    long a = 0, b = 1, c=0;
    if (n <= 1) {
        return n;
    } else {
        for (i = 2; i <= n; i++) {
            c = a + b;
            a = b;
            b = c;
        }
        return c;
    }
}

void main()
{
    int i;
    for(i = 0; i < 26925370; i++)
        Fib(30);
}

fibcs.cs fib2cs.cs

public class A
{
static long Fib(long n)
{
    if (n <= 1) {
        return n;
    } else {
        return Fib(n - 1) + Fib(n - 2);
    }
}

public static void Main()
{
    for(int i = 0; i < 10; i++)
        Fib(30);
}

}

public class A
{
static long Fib(long n)
{
    int i;
    long a = 0, b = 1, c=0;
    if (n <= 1) {
        return n;
    } else {
        for (i = 2; i <= n; i++) {
            c = a + b;
            a = b;
            b = c;
        }
        return c;
    }
}

public static void Main()
{
    for(int i = 0; i < 26925370; i++)
        Fib(30);
}

}

fibjava.java fib2java.java

public class fibjava
{
static long Fib(long n)
{
    if (n <= 1) {
        return n;
    } else {
        return Fib(n - 1) + Fib(n - 2);
    }
}

public static void main(String[] args)
{
    for(int i = 0; i < 10; i++)
        Fib(30);
}

}

public class fib2java
{
static long Fib(long n)
{
    int i;
    long a = 0, b = 1, c=0;
    if (n <= 1) {
        return n;
    } else {
        for (i = 2; i <= n; i++) {
            c = a + b;
            a = b;
            b = c;
        }
        return c;
    }
}

public static void main(String[] args)
{
    for(int i = 0; i < 26925370; i++)
        Fib(30);
}

}

fibjs1.js fib2js1.js

function Fib(n)
{
    if (n <= 1) {
        return n;
    } else {
        return Fib(n - 1) + Fib(n - 2);
    }
}

for(var i:int = 0; i < 10; i++)
    Fib(30);

function Fib(n)
{
    var i;
    var a = 0, b = 1, c=0;
    if (n <= 1) {
        return n;
    } else {
        for (i = 2; i <= n; i++) {
            c = a + b;
            a = b;
            b = c;
        }
        return c;
    }
}

for(var i:int = 0; i < 26925370; i++)
    Fib(30);

fibjs2.js fib2js2.js

function Fib(n:int):int
{
    if (n <= 1) {
        return n;
    } else {
        return Fib(n - 1) + Fib(n - 2);
    }
}

for(var i:int = 0; i < 10; i++)
    Fib(30);

function Fib(n:int):int
{
    var i:int;
    var a:int = 0, b:int = 1, c:int=0;
    if (n <= 1) {
        return n;
    } else {
        for (i = 2; i <= n; i++) {
            c = a + b;
            a = b;
            b = c;
        }
        return c;
    }
}

for(var i:int = 0; i < 26925370; i++)
    Fib(30);

ptime.cs cp.bat

public class A
{
    public static void Main()
    {
        System.Console.Write(System.DateTime.Now.Ticks);
        System.Console.Write(',');
    }

}

@echo off
cl /O2 fibc.c
cl /O2 fib2c.c

csc /o+ /debug- fibcs.cs
csc /o+ /debug- fib2cs.cs

jsc /fast- /debug- fibjs1.js
jsc /fast- /debug- fib2js1.js

jsc /fast+ /debug- fibjs2.js
jsc /fast+ /debug- fib2js2.js

"%JAVA_HOME%\bin\javac" -g:none fibjava.java
"%JAVA_HOME%\bin\javac" -g:none fib2java.java

echo 编译完成

call:run fibc
call:run fib2c
call:run fibcs
call:run fib2cs
call:run fibjs1
call:run fib2js1
call:run fibjs2
call:run fib2js2
call:run "%JAVA_HOME%\bin\java" fibjava
call:run "%JAVA_HOME%\bin\java" fib2java
echo finish!

goto end
:run
echo =================================
ptime & %1 %2 & ptime & echo %1 %2 1
ptime & %1 %2 & ptime & echo %1 %2 2
ptime & %1 %2 & ptime & echo %1 %2 3
ptime & %1 %2 & ptime & echo %1 %2 4

:end

运行测试的结果如下表格所示,表格内部蓝色的4组数据分别为1,2,3,4测试数据,黑色数据为后三组测试结果的平均数,绿色数据为相对C语言运行耗时的比例,最后一行红色纵比数字为相同语言【单次】递归和迭代算法的耗时比例。时间单位为百分之一秒:

 

C

C#

java

js

JScript.net

递归
(10次)

119
49
48
49

486
439
461
441

2258
520
467
464

75397
75327
76424
74228

1571
1501
1502
1499

48.67

447

483.67

75326.33

1500.67

1

9.18

9.94

1547.70

30.83

迭代
(26,925,370次)

120
49
47
46

5261
5041
5040
5039

7880
7769
7762
7766

125786
127117
127273
127541

9196
9101
9086
9121

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代码中间可以优化的地方实在是太多太多了。

结论:

  1. C作为一种老牌的中高级语言,优势没得说。
  2. 递归少用,尤其是JavaScript,连函数调用都尽可能压缩一些。
  3. 本文的最主要目的,如果用JScript.net做一般应用程序,效率应该属于可以接受的范围,但是千万不要进行数值计算。
  4. 以前看过什么java或者C#运行效率可以达到C语言的70%之类的文章,现在看来是有水分的,如果单纯比较编译器的效率,我看差距还是明显的。看来枪手文章还是要警惕啊!

另外给各位看官提一个小小的请求,如果哪位对python,ruby,perl等熟悉的,用相同算法做一个测试如何?

本文章算法参考了浅议Fibonacci(斐波纳契)数列求解

========================================

2007-11-16 17:30修改迭代部分的循环次数为26925370次,重新更正相关测试的时间和部分结论。非常感谢装配脑袋的提醒。谢谢!另外对之前给大家的误导表示歉意!

posted on 2007-11-16 15:24 老翅寒暑 阅读(2033) 评论(34)  编辑 收藏 所属分类: 工具与应用

评论

#1楼    回复  引用    

我想知道 T-SQL 的结果。。。。。。。。
2007-11-16 16:13 | Jade [未注册用户]

#2楼    回复  引用  查看    

不错,喜欢这样的测试对比
2007-11-16 16:15 | jillzhang      

#3楼    回复  引用  查看    

不知道到底比较的对不对,因为我在C#内部用Stopwatch计时,计算Fib(80)并且重复1000000(一百万)次计算,才耗时380毫秒。LZ可以看看提高循环数量是否运行时间线性增长。
2007-11-16 16:32 | 装配脑袋      

#4楼    回复  引用  查看    

Ps.要提醒一下,Fib的迭代算法复杂度仅为O(n),所以Fib(30)根本没有复杂度而言。我测试的就是你的迭代版C#,因此你用迭代版得出的结论恐怕比较的是各个runtime/虚拟机的启动时间吧
2007-11-16 16:34 | 装配脑袋      

#5楼    回复  引用  查看    

class Fib: 
   
def __cal__(self, n): 
   
if(n<=1): 
       
return n 
   
return self.__cal__(n-1)+self.__cal__(n-2

print Fib().__cal__(30

好久没写了,居然还会写 :)
2007-11-16 16:35 | micYng      

#6楼 [楼主]   回复  引用  查看    

@装配脑袋
我的机器vista,cpu双核1.66G,内存1G,结果C#计算fib(50)就等不及了,超过10分钟,这就是为什么我用fib(30)循环10次的原因。

不过迭代好像确实有些少了,我再测试一下
2007-11-16 16:37 | 老翅寒暑      

#7楼    回复  引用  查看    

@老翅寒暑
递归版是很慢,我说的是迭代版
2007-11-16 16:38 | 装配脑袋      

#8楼    回复  引用  查看    

插句题外话,其实斐波那契数列还有O(log(N))的解法,比方说用这个算法计算Fib(80)只要循环8次:
遗憾的是实测中它并不是最快的,因为O(n)版一次循环所做的工作量很小,而且运算的规模也只能维持2位数左右,所以复杂度的优势无法体现。
    Function Fib(ByVal n As CalcType) As CalcType
        ‘CalcType其实就是Long
        
Dim c As CalcType = n
        
Dim a As CalcType = 1
        
Dim b As CalcType = 0
        
Dim p As CalcType = 0
        
Dim q As CalcType = 1
        
Do
            
If c = 0 Then Return b


            
If c Mod 2 = 0 Then
                
Dim p_2 = p * p + q * q 'P2(p, q)
                Dim q_2 = q * q + 2 * p * q 'Q2(p, q)

                p 
= p_2 : q = q_2

                c 
= c \ 2
            
Else
                
Dim a_2 = b * q + a * q + a * p 'Ta(a, b, p, q)
                Dim b_2 = b * p + a * q 'Tb(a, b, p, q)

                a 
= a_2 : b = b_2

                c 
-= 1
            
End If
        
Loop
    
End Function
2007-11-16 16:43 | 装配脑袋      

#9楼 [楼主]   回复  引用  查看    

@装配脑袋
我文章的参考文献里边已经很详细说了。这个哥们算法比我好多了
2007-11-16 16:44 | 老翅寒暑      

#10楼    回复  引用  查看    

其实我主要还是说你比较迭代法的结论问题,因为迭代法计算Fib(30)循环10次绝对是1毫秒也用不到的,再慢的语言也应该差不离。
PS.我帖的方法似乎你的参考文献里也没有:)
2007-11-16 16:51 | 装配脑袋      

#11楼    回复  引用  查看    

JScript跟J#一样,被历史遗忘了
MS也要将它废弃了,发展它不如发展DLR
Python Ruby PHP
都在.Net里面占有一席之地
2007-11-16 17:50 | PureEviL      

#12楼    回复  引用  查看    

*F#*

#light

let sw = new System.Diagnostics.Stopwatch()
let rec fib n:int =
if n <= 1 then
n
else
fib (n-1) + fib (n-2)

sw.Start()
for i = 0 to 10 do
fib 30
sw.Stop()
print_any sw.ElapsedMilliseconds

因为是静态语言~所以很快.. 用int时只要400毫秒左右~
2007-11-16 18:02 | Adrian H.      

#13楼    回复  引用  查看    

如果确实不受你的机器和算法的影响,JScript.net,也太让人意外了
2007-11-16 18:42 | Enzo      

#14楼    回复  引用  查看    

Jscript.net 已经在 2008 里消失了。。。
2007-11-16 18:48 | 随风流月      

#15楼    回复  引用  查看    

我认为 C 语言在计算斐波纳契数列时递归和迭代算法所用的时间相同的原因是 C 语言编译将递归算法优化为迭代算法了,是 C 语言编译器的优化功能极其强大的结果。否则,无论如何,递归算法要比迭代算法慢很多的。
2007-11-16 18:53 | 银河      

#16楼    回复  引用    

嗯,做测试就差不多了,加大参数。如果实际中写出这样的代码,等着被打板子吧
2007-11-16 19:36 | fawfa [未注册用户]

#17楼 [楼主]   回复  引用  查看    

文章更新了一下,原来的结果是有些问题的。
看来.net上运行jscript是没前途了。有空我测试一下rihno吧,看看它编译js的效率如何。
难道没有把java编译成cli的编译器吗?
2007-11-16 23:44 | 老翅寒暑      

#18楼    回复  引用  查看    

楼主这样对待递归,似乎很不公平哦。
首先这样做,递归的计算量就已经是迭代的两倍了(自己想想是不是)。

在我的认识中,迭代的确要比递归快一点,但是不会太多,所以楼上有人提到递归应该比迭代慢很多,我非常不同意。

待会我用我的方法实现一下递归,同时检查一下C语言中递归和迭代的汇编,看看是不是真如楼上所说“C 语言编译将递归算法优化为迭代算法了”
2007-11-17 08:58 | 大石头      

#19楼    回复  引用  查看    

测试好了,先发一部分上来。
static long Fib1(long n)
{
if (n <= 1) return 1;
return Fib1(n - 1) + Fib1(n - 2);
}

static long[] cache = new long[100];
static long Fib2(long n)
{
if (n <= 1) return 1;
if (cache[n] > 0) return cache[n];
cache[n] = Fib2(n - 1) + Fib2(n - 2);
return cache[n];
}

static long Fib3(long n)
{
if (n <= 1) return 1;
long a = 0, b = 1, c = 0;
for (int i = 2; i <= n; i++)
{
c = a + b;
a = b;
b = c;
}
return c;
}

static void Main(string[] args)
{
Stopwatch sw = new Stopwatch();

Fib1(30);
sw.Reset();
sw.Start();
for (int i = 0; i < 100; i++)
Fib1(30);
sw.Stop();
Console.WriteLine("递归一:" + sw.ElapsedMilliseconds + " ms");

cache = new long[100];
Fib2(30);
sw.Reset();
sw.Start();
for (int i = 0; i < 1000000; i++)
{
cache = new long[100];
Fib2(80);
}
sw.Stop();
Console.WriteLine("递归二:" + sw.ElapsedMilliseconds + " ms");

Fib3(30);
sw.Reset();
sw.Start();
for (int i = 0; i < 1000000; i++)
Fib3(80);
sw.Stop();
Console.WriteLine("迭代:" + sw.ElapsedMilliseconds + " ms");

Console.ReadKey();
}
debug运行结果:6102ms 7543ms 836ms
release运行结果:4227ms 6434ms 535ms

从运行100次Fib1(30)来看,楼主的机子要比我机子快多了,不过没关系,我这是为了比较,大家可以在自己的机器上运行看看。
可以看到,优化后的递归二,速度上远远超越楼主的递归一,甚至逼近迭代算法。这里看出,递归二大概是迭代的十倍时间。为什么我的比较方式和楼主的不同?我觉得楼主那样比较不怎么合理,不管怎么算,我都要求一百万次Fib(80)的计算结果,大家只要main中的循环次数相同以及Fib的参数相同就可以了。

我优化的递归二,每次都重新分配缓存数组,避免了不同循环次数之间共用。我使用缓存数组,大家不要说我阴险,事实上这是算法比赛中常用的手段,看看ACM比赛的例子就知道了。
2007-11-17 09:37 | 大石头      

#20楼    回复  引用  查看    

C代码:

long Fib(long n)
{
if (n <= 1) {
return n;
} else {
return Fib(n - 1) + Fib(n - 2);
}
}

long Fib2(long n)
{
int i;
long a = 0, b = 1, c=0;
if (n <= 1) {
return n;
} else {
for (i = 2; i <= n; i++) {
c = a + b;
a = b;
b = c;
}
return c;
}
}



int _tmain(int argc, _TCHAR* argv[])
{
int i;
for(i = 0; i < 10; i++)
Fib(30);
return 0;
}

对应的汇编:
.text:004113B0 ; __int32 __cdecl Fib(__int32)
.text:004113B0 Fib proc near ; CODE XREF: Fib(long)j
.text:004113B0
.text:004113B0 var_C0 = byte ptr -0C0h
.text:004113B0 n = dword ptr 8
.text:004113B0
.text:004113B0 push ebp
.text:004113B1 mov ebp, esp
.text:004113B3 sub esp, 0C0h
.text:004113B9 push ebx
.text:004113BA push esi
.text:004113BB push edi
.text:004113BC lea edi, [ebp+var_C0]
.text:004113C2 mov ecx, 30h
.text:004113C7 mov eax, 0CCCCCCCCh
.text:004113CC rep stosd
.text:004113CE cmp [ebp+n], 1
.text:004113D2 jg short loc_4113DB
.text:004113D4 mov eax, [ebp+n]
.text:004113D7 jmp short loc_4113FD
.text:004113D9 ; ---------------------------------------------------------------------------
.text:004113D9 jmp short loc_4113FD
.text:004113DB ; ---------------------------------------------------------------------------
.text:004113DB
.text:004113DB loc_4113DB: ; CODE XREF: Fib+22j
.text:004113DB mov eax, [ebp+n]
.text:004113DE sub eax, 1
.text:004113E1 push eax ; __int32
.text:004113E2 call j_?Fib@@YAJJ@Z ; Fib(long)
.text:004113E7 add esp, 4
.text:004113EA mov esi, eax
.text:004113EC mov ecx, [ebp+n]
.text:004113EF sub ecx, 2
.text:004113F2 push ecx ; __int32
.text:004113F3 call j_?Fib@@YAJJ@Z ; Fib(long)
.text:004113F8 add esp, 4
.text:004113FB add eax, esi
.text:004113FD
.text:004113FD loc_4113FD: ; CODE XREF: Fib+27j
.text:004113FD ; Fib+29j
.text:004113FD pop edi
.text:004113FE pop esi
.text:004113FF pop ebx
.text:00411400 add esp, 0C0h
.text:00411406 cmp ebp, esp
.text:00411408 call j___RTC_CheckEsp
.text:0041140D mov esp, ebp
.text:0041140F pop ebp
.text:00411410 retn
.text:00411410 Fib endp



.text:00411430 ; __int32 __cdecl Fib2(__int32)
.text:00411430 Fib2 proc near ; CODE XREF: .text:004110FFj
.text:00411430
.text:00411430 var_F0 = byte ptr -0F0h
.text:00411430 c = dword ptr -2Ch
.text:00411430 b = dword ptr -20h
.text:00411430 a = dword ptr -14h
.text:00411430 i = dword ptr -8
.text:00411430 n = dword ptr 8
.text:00411430
.text:00411430 push ebp
.text:00411431 mov ebp, esp
.text:00411433 sub esp, 0F0h
.text:00411439 push ebx
.text:0041143A push esi
.text:0041143B push edi
.text:0041143C lea edi, [ebp+var_F0]
.text:00411442 mov ecx, 3Ch
.text:00411447 mov eax, 0CCCCCCCCh
.text:0041144C rep stosd
.text:0041144E mov [ebp+a], 0
.text:00411455 mov [ebp+b], 1
.text:0041145C mov [ebp+c], 0
.text:00411463 cmp [ebp+n], 1
.text:00411467 jg short loc_411470
.text:00411469 mov eax, [ebp+n]
.text:0041146C jmp short loc_4114A4
.text:0041146E ; ---------------------------------------------------------------------------
.text:0041146E jmp short loc_4114A4
.text:00411470 ; ---------------------------------------------------------------------------
.text:00411470
.text:00411470 loc_411470: ; CODE XREF: Fib2+37j
.text:00411470 mov [ebp+i], 2
.text:00411477 jmp short loc_411482
.text:00411479 ; ---------------------------------------------------------------------------
.text:00411479
.text:00411479 loc_411479: ; CODE XREF: Fib2+6Fj
.text:00411479 mov eax, [ebp+i]
.text:0041147C add eax, 1
.text:0041147F mov [ebp+i], eax
.text:00411482
.text:00411482 loc_411482: ; CODE XREF: Fib2+47j
.text:00411482 mov eax, [ebp+i]
.text:00411485 cmp eax, [ebp+n]
.text:00411488 jg short loc_4114A1
.text:0041148A mov eax, [ebp+a]
.text:0041148D add eax, [ebp+b]
.text:00411490 mov [ebp+c], eax
.text:00411493 mov eax, [ebp+b]
.text:00411496 mov [ebp+a], eax
.text:00411499 mov eax, [ebp+c]
.text:0041149C mov [ebp+b], eax
.text:0041149F jmp short loc_411479
.text:004114A1 ; ---------------------------------------------------------------------------
.text:004114A1
.text:004114A1 loc_4114A1: ; CODE XREF: Fib2+58j
.text:004114A1 mov eax, [ebp+c]
.text:004114A4
.text:004114A4 loc_4114A4: ; CODE XREF: Fib2+3Cj
.text:004114A4 ; Fib2+3Ej
.text:004114A4 pop edi
.text:004114A5 pop esi
.text:004114A6 pop ebx
.text:004114A7 mov esp, ebp
.text:004114A9 pop ebp
.text:004114AA retn
.text:004114AA Fib2 endp

可以看到,不仅在代码上递归要比迭代短,容易理解,就算在汇编上,也远远短得多,并且少了许多计算,递归唯一比迭代多的一个就是压栈一个参数。此外,这个递归,也和C#那样犯了同样的错误,如果加上数组缓存,不知道性能如何,和迭代相比,谁胜谁负还是未知数呢。
2007-11-17 09:57 | 大石头      

#21楼 [楼主]   回复  引用  查看    

@大石头
分析很仔细,赞一个!

我在文中已经把函数调用的差别平衡掉了,这个也是为什么我迭代次数设置的原因,而且在算法分析的表中,也可以看出迭代和递归的差别。本来我想通过分析来评估各个语言之间函数调用效率的差别,但是没想到好办法,只好先分析一下排除调用之后的执行效率差别。

但是你用数组缓存的效率可能会比调用要好一些,但是我觉得也不会好多少(空间换时间而已,递归过于深入的话,就会有另外内存分配效率问题)。不过你这样做提醒了我,因为函数调用还有一个stackframe的问题(支持异常的架构)。

看来这个分析工作还可以做的更加深入些。
2007-11-18 11:25 | 老翅寒暑      

#22楼    回复  引用  查看    

VS 2008中没有JScript.NET了是因为JScript.NET 2.0就要出来了,编译器和运行时库将改成用VB10.0编写,将在下一代VS中面世
2007-11-19 08:38 | 装配脑袋      

#23楼    回复  引用  查看    

@大石头
Good
2007-11-19 12:55 | 周银辉      

#24楼    回复  引用  查看    

实际上,所谓递归法为何速度慢,就是因为递归法重复计算了不应该计算的部分。比方说Fib(30)应当能从Fib(29)和Fib(28)计算出来,同时Fib(29)应当能从Fib(28)和Fib(27)算出来,递归发错就错在令这一步中的Fib(28)计算了两次,没有利用已经算出来的值;这样引发下去成了O(Exp(N))的爆炸重复算法。这压根不是用递归和循环这两种手法写代码有什么区别,而是“递归法”和“迭代法”求Fib的算法根本就是不同的算法。所以说大石头把结果用数组缓存下来就是想用这个法子减少重复计算,遗憾的是没有发现迭代法是怎么真的消除重复计算的。
2007-11-19 15:14 | 装配脑袋      

#25楼    回复  引用  查看    

用Lisp写出两个算法就更能看到本质:这两个一个是递归法,一个是迭代法,但都必须用到递归(因为Lisp只能递归)。但两者求值序展开是完全不同的

(define (fib n)
 (cond((= n 0) 0)
    ((= n 1) 1)
    (else (+ (fib(- n 1))
        (fib(- n 2))))))

(define (fib-iter a b count)
 (if (= count 0)
   b
   (fib-iter (+ a b) a (- count 1))))
2007-11-19 15:27 | 装配脑袋      

#26楼    回复  引用  查看    

我怎么看上面有人理解为:
递归的速度比迭代要快?比如就有人列举汇编结果,认为递归的代码更简单,运算量更小,所以更快。也许我理解错误了,但是反正有不少回复给我的感觉就是如此。

事实上这种理解是错误的,因为以C语言的结果为例,在几乎相同的时间下面,递归只算了10遍,而迭代运算了26925370。这么说也许还是不太清楚,这么说吧:
假如一颗导弹上面的导航系统,恰好要算Fib,每秒钟计算次数越多越精确,你觉得用递归的导弹好呢,还是迭代的导弹好?
要我我用迭代的。

列出汇编的同志说,如果递归使用缓存可能更快。确实应该更快,但是事实上,如果使用了缓存,也就等同于迭代中的对a和b进行赋值及取值。同样的,迭代也可能存

在更好的优化方案。因此这种最终是否能够优化得跟迭代一个数量级,很难讲,而且优化之后,是否还能保持原来所谓的“可读性”优势,也很难讲。可读性本身是一

种无法简单衡量的问题,古人读古文就很可读,现代人读古文就很难读。关键在于一种写法对于读的人来说,是否熟悉。大多数人读迭代容易还是读递归容易,我觉得

很难说。对于你我来讲,可能都差不多,一看就知道什么意思了。对于初学者,也许递归更难理解一点。

对于楼主,你的这个比较很有意思。但是写的却是有点误导别人,为什么这么说呢?按照我的理解,你的分析过程分为如下三个步骤:

1、也许你一开始,想要比较递归和迭代的差距。
2、后来发现差距挺大的,就假设是调用引起的,于是让大家都进行了同样多次的调用,试图抹去call操作引起的差别。(其实这里已经偏离了你原来做本次试验的目标

——检验递归和迭代之间的差距,因为你抹掉了call差异的同时,扩大了其他运算量的差异!)
3、最后发现结果和想象的还是不一样,才找到运算量的差异。

第一步的问题其实很简单,本来就是递归比迭代要费时间。简单的时间复杂度分析即可得出结论。
第二步的试验目的就是抹去call操作引起的差别,结果出来之后,还是不能够自圆其说。
第三步,发现了运算量差异,但是没有说明白,或者可能没有想明白。

其实应该是这样的,参见最后一张表:
递归 迭代
赋值语句 3 120
变量分配 2 4
函数调用 2 0
返回 2 2
条件判断 1 30
跳转 1 30
累计 11 186

其实这么一张表是非常有误导性的,因为递归的一次调用,只是计算一次斐波纳契数列所需要调用的N次Fib()中的一次调用。
而迭代的数据,则是计算一次斐波纳契数列的所有运算量。
这跟用一颗子弹和一颗原子弹的制造成本,来评估他们的作战成本一样。

事实上,迭代方法所做的运算总量,减去递归的运算总量,剩下来的是26925370 - 10=26925360 次的除了Call操作之外的迭代运算。
而试演的最终结果,也能说明好几个问题:

1、通过C的运算结果看,call操作造成了极大的性能损失(损失了26925370次迭代运算的时间),为什么?
因为Call操作会中断CPU的分支预测过程,因此虽然Call操作数量一样,在同等甚至更少的时间内,迭代运算多计算了26925360次的Fib(30)!

2、通过C#的运算结果看,迭代比递归多出来的时间倍数,和一次调用的运算量差异几乎成正比。也就是说,C#的迭代比递归“慢”,
是因为每次调用的运算量差异。实际上,如果我们比较每次Fib运算,还是迭代比递归快很多的,即使是C#。

3、通过C和C#的对比发现,在递归时,由于大家在CPU分支预测上的差异相对较小,而迭代时双方对分支预测的效果差别会比较大。
如果你看过C#程序的实际机器代码,就会发现他里面会夹杂不少的Call操作,用来比如说判断是否超界等,具体这个例子如何,不太清楚。
因此很可能C#的性能损失发生在这里。

反正我得出的结论和大家的不太一样,不知道是否有不对的地方,请指正。
2007-11-20 14:10 | Sumtec      

#27楼    回复  引用  查看    

楼上的兄弟,误解我的意思了。

从学编程开始,这十年时间里,我一直都认为递归比迭代要慢。
我说那么多,只是想说明,递归并不是那么不堪,比迭代慢那么多,楼主测试出来的差距实在太大太大了。
2007-11-20 17:26 | 大石头      

#28楼    回复  引用  查看    

迭代,就是指“尾递归”,迭代算法照样可以写成递归的,但必定是尾递归的。现代编译器都能把尾递归转换成循环表述。
迭代的实现方面,尾递归之所以比循环慢是因为栈的操作远比跳转指令要费时。这完全是迭代的实现方法内部讨论,根本不关“真递归”算法的事情。真递归算法必定有真正利用到栈的地方。
将真递归算法转变为循环类实现也照样是完全可能的,比方说快速排序的通常实现都是没有函数递归操作的。但这样未必就比使用递归操作来实现性能更好,和尾递归相比,真递归算法总是要用到栈,自己实现的栈未必比编译器实现的更快。但是非递归的实现仍然很重要,因为它不会把较小的调用栈用尽而出错。

综上所属,迭代和递归这一对是指算法本身的特性,迭代就是指尾递归。而递归实现与非递归实现完全是另一码事。大家讨论的时候不要搞混。
2007-11-20 17:49 | 装配脑袋      

#29楼    回复  引用  查看    

Fib两种实现的速度差异,是因为两种“算法”本身就是不同的,一种是O(2^N)的算法,被人称作“递归法”,一种是O(N)的被人称作“迭代法”的算法。说到这份上应该没有歧义了吧……
2007-11-20 18:02 | 装配脑袋      

#30楼    回复  引用    

为什么在我的机器上运行速度不是这样的呢??
我的Vs 2005, C++的速度 递归 1.593s 迭代 4.875s
JDK1.6 速度为:递归 0.163s 迭代 5.68s
请问你用的JDK是什么版本啊?不会是JDK1.4的吧?
2007-12-02 01:49 | tkw [未注册用户]

#31楼 [楼主]   回复  引用  查看    

@Sumtec
赞一个,确实写到后来发觉纵向比较有很大的问题。不好下结论。有时间彻底想明白之后再来补充一篇。
2007-12-10 22:55 | 老翅寒暑      

#32楼    回复  引用    

博主的比较方法存在严重问题,把相应加载程序与加载库的时间都算进去了,往往这个时候程序运行消耗的时间比加载程序与库的时间会小很多。个人建议把时间计算写在程序里面,这样可以避免加载的时间进入测试结果。
2007-12-12 13:16 | bangbang [未注册用户]

#33楼    回复  引用  查看    

能准确吗?
2008-04-22 19:40 | 簡簡單單..      

标题  
姓名  
主页
Email (博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
该文被作者在 2007-11-16 23:56 编辑过


相关链接:

历史上的今天:
2006-11-16 维基百科开放拉
2004-11-16 Map,一个存放可重复key的无序列表