本文适合初学编程的程序员阅读,它对比了几种编程语言在解决同一问题的时候的运效率。并通过具体的例子进行了量化分析。主要目的是帮助初学者认识各种编程语言的特质,并且能够理性的选择适合的编程语言来进行工作。
事发
我无聊的翻着散落案头的书籍,这些都是五花八门的关于编程和系统管理的著作。干了这么多年程序员,大大小小的软件和项目也做了无数。每每有新入行的朋友问我这个所谓的"老前辈":哪种语言最好之类的问题,我总会作出一副知识渊博的样子,复述着从更老的老前辈那里听来的或者某些名著上看来的"知识"。就好比我们从学习编程的第一天起,就被计算机老师告知,COBOL语言是擅长处理商务事务、FOTRAN语言是用于科学计算一样。类似的知识还有"汇编语言比C语言快得多"以及"JAVA是一种效率很低的语言环境"在一代又一代的程序员中口耳相传,几乎成为了毋庸置疑的真理。
我产生了一个想法,能不能对于同一个应用用几种编程语言分别实现,来比较一下看看到底哪种语言效率最高?
老实说我自己都觉得这个想法很无聊,想想谁会反复用不同的语言写同一个程序呢?下雨天打孩子,闲着也是闲着。再说,对于某种语言的弱点和优势有一个量化的分析,对于我们今后在做项目的时候面临工具选择也少许有一点指导意义。另外,觉得好玩才是我做这件事情的真正原因。
选题
选择一个什么样的程序问题进行这样的测试呢?这是一个很关键的问题,也最容易影响测试的公平性。另外的,对于每种语言,各自的优势都是不同的。程序员的偏爱也是各不相同的。在网上和现实中,对于什么语言更好一些的争论从来就没有停止过。甚至的,各门各派的程序员所构成的各种阵营,把某种语言奉若神明的也不在少数。不信,你在CSDN的JAVA论坛说一句"JAVA执行效率太低了云云"试试?立刻会被铺天盖地的板砖掀翻在地。类似的,还有管理员对于操作系统的偏好和争论:在Linux论坛你要是表扬Windows,其惨烈程度简直是难以言状。因此,从这个意义上来说,程序员们对于编程语言的偏好,类似于战士之喜爱枪械,赛手之喜爱赛车,已经上升为一种精神层面的东西了。蔡学镛先生说得好:有人逢微软必反,有人逢微软必捧。这是一种纯粹的精神上的爱,但它可能会影响正常的、科学的思考。
可以预料的,我这篇文章一定会遭到各路豪杰的迎头痛击。
好了,让我们言归正转吧。首先的,我们的选题中要使用的各种程序语言的最常用的要素。什么是最常用的要素呢?当然了,大家都有的就是赋值、数组操作、循环、判断等。另外,对IO的操作也是编程语言重要的内容。其次的,操作时间一定要长,否则,对于解释性的语言来说是极不公平的:解释器还没调入内存呢,人家编译派的已经运行完了。最后,就是程序不能太复杂。除了我没有那么大的毅力用各种语言完成一个复杂算法的决心外,程序过于复杂,算法在测试中起的作用就越来越大,影响运行效率的原因也就增加了。算法过于复杂,开发工具的扩展部分用得也就越多。于是就成了语言附加库之间的竞赛了,这是我不愿意看到的。
考虑上述因素,我设计了一个简单的选题:从指定文本文件中搜索指定字符串,计算个数。并且打印出搜索到的个数作为结果输出。作为程序员的你粗粗过一下脑子,马上会想到这个算法里面包含了条件判断、循环、数组操作等基本的程序语言因素。这满足了上面第一个条件。另外的,为了满足第二个条件,我准备了一个多达2G的文本文件,总共有文本1500万行多。这保怔了足够的运行时间(但应该不会太长),而决不会一眨眼就执行完了。最后的,我们都知道,在文本串里面搜索子串的算法是数据结构课本中的一个典型的例子(考试也经常被考到的),也满足算法简单的要求。同时,为了让每个程序的环境都一样,我得每测试一次就重新启动一次机器,避免CACHE的影响。进入讨论组讨论。
准备
比赛嘛,就需要公平。首先的,硬件平台要统一。我找了一台看起来还不错的机器(服务器):两颗PIII800,1G内存。操作系统嘛,原来的机器上有新装的Windows2000Server版本。几乎没装什么别的应用。我偷懒了一下,没有重新安装OS,就这样用吧。
第一个选手:PERL
如果别人交给我这个题目,我会马上决定用PERL语言来做这件事。这个题目是完全的文本处理问题,还有比用PERL来做更合适的吗?因为PERL是专门为了文本处理而编制的语言。事实上也是这样,我用了2分钟,写了几行代码,就轻松实现了这个问题。这也说明了,选择适用的编程语言工具,比选择喜爱的工具更重要。
$filename="d:\\access.log_";
$count = 0;
open(FILE , "<$filename");
while(<FILE>)
{
@match_list = ($_ =~ /HIT/g);
$count=$count+@match_list;
}
close(FILE);
print "Count = $count\n";
exit
第二个选手:纯C
也许年龄大了,但是我真的很喜欢C语言。而且我最喜欢的就是使用指针和强制类型转换来任意操作数据。我甚至会在程序里通过指针手工拼凑一个长整性的数据。说句可能引起争议的话,我觉得JAVA语言抛弃可爱的指针的做法基本上就是逃避。因为掌握不好就不用,到头来就是牺牲了效率。
本文这个题目,用C语言来实现应该还是比较不错的选择。下面的代码就是在VC下面实现的纯C代码的字符串搜索程序(为了避免图形界面的干扰,一律做成控制台程序)。编译的时候使用速度优先编译选项。
#include <string.h>
void main()
{
int len=2048;
char filename[20];//文件名
char buff[10000];//文件缓冲区
char hit[5];
FILE *fd;
int i,j,flag=0,over=0;
int max,readed;
int count=0;//最后的结果
strcpy(&filename[0] , "d:\\access.log_");
strcpy(&hit[0] , "HIT");
buff[0]=0x0;
buff[1]=0x0;
//打开文件:
if((fd = fopen(&filename[0] , "rb"))==NULL)
{
printf("Error : Can not open file %s\n",&filename[0]);
}
//读取文件内容
while(over != 1)
{
readed = fread(&buff[2] , 1 , len , fd);
if(readed < len)
{
over=1;
max=readed;
}
else
{
max=len;
}
for(i=0;i<max;i++)
{
for(j=0;j<3;j++)
{
if(hit[j] != buff[i+j])
{
flag=0;//一旦有一个不相同就退出并且标志为0
break;
}
else
{
flag=1;//一个相同为1,如果连续都相同最后结果定是1
}
}
if(flag==1)
{
count++;
i+=j-1;
}
else
{
if(j==0)
{
i+=(j);
}
else
{
i+=(j-1);
}
}
}
//把最后两个字符转移到前面两个字节以防止切断搜索串.
buff[0]=buff[max];
buff[1]=buff[max+1];
}
fclose(fd);
printf("count:%d\n",count);
}
程序很好懂,用的也是教科书上面的标准字符串搜索算法,但是比前面的PERL程序长多了吧?那是因为人家PERL已经帮你完成了大部分工作。但是看到上面这段程序的运行结果你可能会高兴起来,它最快一次只用了2分10秒52,最慢也只用了2分20秒59就完成了1500万行文本的搜索任务。平均2分15秒多。为什么每次时间不一样呢?我不清楚具体原因,但学过操作系统的朋友会明白,只有在单道单任务的系统中,代码才能有执行上的可再现性。
有经验的朋友可能会说,你的缓冲区只用了2048字节,加大它速度还会增加呢。是的,而且我相信还有高手能作出更快的程序来,但这不重要,重要的是我们要考察的是不同语言完成同一件工作的效率。而且你能够明白,在程序中,改进什么能够提高效率,这就足够了。因为C语言程序中,这些都是自由可控的。
第三个选手:C++
C++和前面的C是亲戚。我简单的把前面的C代码移植过来,然后把文件输入部分改成了流类对象。至于算法部分嘛。跟前面的C是一模一样的。最后在编译的时候,除了使用速度最佳编译选项外,当然还用了C++的编译参数,因此执行文件的长度比前面的C要长一些,这说明我加的流类代码比标准C库要复杂。是的,C++应该说是目前流行的计算机编程语言中复杂度排名靠前的。其复杂的类和继承关系,以及各种初始化的次序和构造函数执行顺序等都需要考虑。还有多态以及动态联编技术等。C++也是我非常喜欢的语言,提供了面向对象的代码重用特性和足够的安全型,但是在效率上的确比纯C略逊一筹。你知道吗,大部分的操作系统核心几乎都是用纯C写成的,尽管很复杂,但很少有使用面向对象技术的。为什么,不是面向对象技术不好,也不是操作系统核心不够复杂(那什么复杂?),主要的考虑就是效率问题。
这段C++程序在测试平台上用了最快4分25秒95 到最慢5分40秒68的时间完成1500万行的文本检索,并在2G的文件中检索出10951968个"HIT"字符串。这结果是正确的。
第四个选手:汇编
本以为汇编程序能够达到前所未有的高速,把前面的选手远远抛在身后而笑傲江湖。这一想法支撑我完成了艰涩的代码。可事实上测试的结果缺让我大失所望,完全用机器指令书写的程序,去掉缓冲区才几百字节,算法和前面的C程序一模一样,扫描1500万行文本竟然最快也要2分14秒56!这甚至还比不过C语言的最快纪录。而平均下来,汇编程序的速度竟然和前面的C程序在伯仲之间。恐怕这样的结果也出乎大部分人的意外。因为我们从入行的那一天起,就被告知汇编是你所能够掌握的最快的语言!尽管代码坚涩难懂,但性能的代价是值得的。而从这里的测试看,你觉得向下面这样的代码,实现和C语言一样的速度和功能值得吗?
STSG SEGMENT STACK 'S'
DW 64 DUP(?)
STSG ENDS
;数据段
DATA SEGMENT
rlength EQU 2048
fname DB 'access.log_',0
hit DB 'HIT$'
fd DW ? ;文件句柄
resault DB 'count : $' ;结果提示
count DD 0 ;存放结果
disflag DB 0 ;显示标志
buff DB 5000 dup(0) ;缓冲区
DATA ENDS
;代码段
CODE SEGMENT
MAIN PROC FAR
ASSUME CS:CODE,DS:DATA,SS:STSG,ES:NOTHING
MOV AX,DATA
MOV DS,AX
;我的代码开始:
mov ah,3dh ;打开文件
lea dx,fname
mov al,00h ;文件打开方式
int 21h ;开始操作
;这里就不作错误处理了,偷懒喽!
;CF=0表示正确,CF=1表示错误,AX是文件句柄或者是错误代码
mov fd,ax ;保存文件句柄
READ: mov ah,3fh ;读文件
mov bx,fd ;文件句柄
mov cx,rlength ;要读length字节
lea dx,buff ;给出读缓冲区指针
add dx,2 ;缓冲区指针向后错两个(目的是解决边界问题:有一个HIT正好横跨rlength界限)
int 21h ;开始读
;AX里面是实际读出的字节数
;读完了以后,扫描缓冲区
push ax ;保存AX字节数
cmp ax,0
jz ALLEND ;文件读完了就退出
sub dx,2 ;指针向前错2个,
mov si,dx
add dx,2 ;把指针回到原来的位置
add dx,ax ;计算结尾
LOD3: cmp si,dx ;到头了就重新读一次文件
jz OVR
lods buff
lea bx,HIT
cmp al,[bx]
jnz LOD3 ;读第一个字节不相等就重新读一个
cmp si,dx
jz OVR
lods buff
cmp al,[bx+1]
jnz LOD3 ;如果第一个字节相等,就读第2个字节,不行等就从第一个字节再重比较。
cmp si,dx ;如果第二个字节也相等的话,就比较第三个字节。
jz OVR
lods buff
cmp al,[bx+2]
jnz LOD3 ;第三个字节不相等再从头开始
;有一个HIT匹配
push bx
lea bx,count
add WORD ptr [bx],1 ;计数器增加一个
adc WORD ptr [bx+2],0 ;进位
pop bx
jmp LOD3
OVR: mov ah,[si-1]
mov BYTE ptr buff+1 , ah
mov ah,[si-2]
mov BYTE ptr buff , ah
pop ax ;恢复这次总共读出的字节数
cmp ax,rlength ;看看是不是最后一次(剩余的零头)
jz READ
;如果是最后一次读文件,
ALLEND: mov ah,3eh ;关闭文件
mov bx,fd ;文件句柄
int 21h ;关闭文件
mov ah,9 ;显示结果字符串
lea dx,resault
int 21h
;转换2进制结果到10进制ACSII形式
mov bx, WORD ptr count
call TERN
mov ax,4c00h ;返回DOS
int 21h
;结束代码,最大的数字已经排到了最前面
MAIN ENDP
TERN PROC ;这个子程序是转换并显示2进制数字的
mov cx,10000
call DEC_DIV
mov cx,1000
call DEC_DIV
mov cx,100
call DEC_DIV
mov cx,10
call DEC_DIV
mov cx,1
call DEC_DIV
ret
TERN ENDP
DEC_DIV PROC
mov ax,bx
mov dx,0
div cx
mov bx,dx
mov dl,al
add dl,30H
mov ah,disflag ;read flag
cmp ah,0
jnz DISP ;已经显示过有效数字了
cmp dl,30H
jz NODISP
mov disflag,1 ;作用是第一个有效数字出现前不显示0
DISP: mov ah,2
int 21H
NODISP: ret
DEC_DIV ENDP
CODE ENDS
END MAIN
上面这段代码我猜你也懒得仔细阅读。其实他不能"显示结果"。因为最后这段负责把最终结果转换成可显示ASCII码的程序实际上只能转换二进制十六位的数据,而最终的结果高达1000万挂零,显示会出错。由于这最终结果的显示已经和程序的运行没有大关系了,因此,我也就懒得去写一个32位的ASCII转换程序了。就这样吧。
第五个选手:JAVA
JAVA是一个不能不参加比赛的选手。有如此多的人热爱他,他们中的一半人是因为JAVA的面向对象特性以及良好的跨平台特性。而另一半人纯粹就是因为JAVA不姓"微(软)",这就是意识形态在程序员头脑中对某种语言的注释。单纯从语言元素上来说,我还是比较喜欢JAVA的。因为他的语法干净、简洁。环境也好。虽然用虚拟机系统(JVM)的做法来实现跨平台特性并非什么了不得的创意(像不像30年前的BASIC解释器?别跟我说什么中间代码?几乎所有的解释器都是把语言因素翻译成中间代码的,JVM不过是分成2步来实现罢了,但从运行机制上应该是差不多的。),但JVM仍然将JAVA的跨平台特性做到了前所未有的地步。而且JVM是一个很干净的系统,让人用起来赏心悦目。说到这里我忍不住想提一下J2EE企业应用框架了。不知道有多少人能够看懂SUN出的J2EE的"理论著作"?满纸充斥着各种生造的概念,洋溢着溢美之词。JAVA的企业应用框架实在是比较复杂的东西,虽然赶不上后来的.NET框架,但足以让大多数初学者望而却步。一句话,东西太多了。事实上JAVA的企业级应用并没有想象的成功,iPlanet就随着电子商务概念的全面垮台而渐渐淡出。现在换了个名叫“SUNONE”――SUN公司员工原话。
我们回到JAVA的语言元素上来说,实际上JAVA可以被理解为被纯化的C++。JAVA去除了C++为了兼容C而增加的一些"非面向对象特质",用其他的一些变通办法实现C++直接实现的功能,比如:多继承。在实现机制上,JAVA的程序会先编译成.CLASS文件,然后这种跨平台的中间代码就可以"一次编译,到处运行"了。当然必须运行在有JVM虚拟机的环境中,连图形什么的都可以照搬。换句话说,你用JAVA程序在PC屏幕上画一个圆,在JAVA-PDA上它还是圆的。
我在本次测试中,写了下面的代码,用JAVA做了同样的测试,测试中实际上用到了:JAVA的文件流类,运行了循环、条件判断、数组操作等基本的语言因素。环境是J2SE1.3.1-06。JAVA程序做1500万行的文本扫描用了8分21秒18。应该说是几种语言中最慢的,基本上和纯解释的PERL是在同一水准。J2EE的JVM环境还是经过优化的所谓HOTSPOT。
public class langtest
{
public static void main(String[] args)
{
String filename = "d:\\access.log_";
try
{
count(filename);
}
catch (IOException e)
{
System.err.println(e.getMessage());
};
}
public static void count(String filename) throws IOException
{
long count=0;
long len;
String strline = "";
char hit[] = {'H','I','T'};//要搜索的字符串
char buff[] = new char[2100];
Reader in = new FileReader(filename);//用FileReader类构造产生一个Reader类对象
LineNumberReader line = null;//生成一个空指针
try
{
line = new LineNumberReader(in);//建立LineNumberReader类对象
while((strline = line.readLine()) != null)
{
//到这里已经读出一行了,用下面的代码分析这行有几个HIT
int i=0,j=0,max=0,flag=0;
buff = strline.toCharArray();//转换成字符数组
max = strline.length();
for(i=0;i<max;i++)
{
for(j=0;j<3;j++)
{
if(hit[j] != buff[i+j])
{
flag=0;//一旦有一个不相同就退出并且标志为0
break;
}
else
{
flag=1;//一个相同为1,如果连续都相同最后结果定是1
}
}
if(flag==1)
{
count++;
i+=j-1;
}
else
{
if(j==0)
{
i+=(j);
}
else
{
i+=(j-1);
}
}
}
}
System.out.println("Count : "+count);
}
catch (IOException e)
{
System.err.println(e.getMessage());
}
finally
{
try
{
if(in != null) in.close();
}
catch (IOException e)
{
}
}
}
}
候捷先生翻译的宏篇巨著《JAVA编程思想》一书中第67页说到:"使用最原始的JAVA解释器,JAVA大概比C慢上20到50倍"之说法我在阅读的时候就心存疑虑,心想要是这样,JAVA完全没有存或与世间的必要了。在亲自动手试验过后,我觉得说JAVA在J2EE环境下,比C慢上2-3倍还是比较可靠的说法的。况且,目前越来越多的硬件JVM的诞生,也给JAVA越来越多的机会。不过我担心的正是这点,JVM的多厂家多样化很可能会造成某些兼容性方面的问题。例如我见过一篇文章就是讨论某种JAVA程序在IBM-JVM可用而在SUN-JVM上不可用之事例。但愿的,JAVA能健康成长。
总结
事实上,本文有两个基本的意义传递给初做程序员的读者:
一、 抛开你的意识形态好恶,选择最合适的编程语言来完成你的工作。每种流行的语言都有自己存在的意义。
二、 在编程中,有想法就自己做一做,你会得出自己的结论。
至此,你应该明白,前面的所有测试结果其实并不重要,重要的是你了解了这些语言的特质,也许在今后的编程生涯中会因此增加一点点"经验"呢。
后记
本来笔者还打算继续测试一下另外的一种颇为流行的解释语言Python和新贵C#以及在Linux平台完成这些测试,但终究还是被懒惰瓦解了斗志。好在的,Python和Perl比较相似,而C#和JAVA有异曲同工之妙。也可以略略做一点参考。
事实上,本文测试中有一个大大的不公平之处,相信仔细的读者已经发现了:其中C和ASM都是使用缓冲区直读的办法,不管三七二十一就进行判断(最后用指针检查缓冲区边界)。而C++等其他的语言虽然用了非常方便的流按行读出,但是多做了很多事情:每一个字符都要判断其是不是回车换行符,而按行读近来,每次缓冲的也要少很多。因此其他几种语言就大大的吃亏了。不过这并不影响结论性的东西,因为测试本身就说明越方便就效率越低。事情总是要有人做,不是吗?