C/C++/Perl/汇编/Java效率比较(zz)


 本文适合初学编程的程序员阅读,它对比了几种编程语言在解决同一问题的时候的运
行效率。并通过具体的例子进行了量化分析。主要目的是帮助初学者认识各种编程语言
的特质,并且能够理性的选择适合的编程语言来进行工作。

  事发

  我无聊的翻着散落案头的书籍,这些都是五花八门的关于编程和系统管理的著作。
干了这么多年程序员,大大小小的软件和项目也做了无数。每每有新入行的朋友问我这
个所谓的"老前辈":哪种语言最好之类的问题,我总会作出一副知识渊博的样子,复述
着从更老的老前辈那里听来的或者某些名著上看来的"知识"。就好比我们从学习编程的
第一天起,就被计算机老师告知, COBOL语言是擅长处理商务事务、FOTRAN语言是用于
科学计算一样。类似的知识还有"汇编语言比C语言快得多"以及"JAVA是一种效率很低的
语言环境"在一代又一代的程序员中口耳相传,几乎成为了毋庸置疑的真理。

  我产生了一个想法,能不能对于同一个应用用几种编程语言分别实现,来比较一下
看看到底哪种语言效率最高?

  老实说我自己都觉得这个想法很无聊,想想谁会反复用不同的语言写同一个程序呢?
下雨天打孩子,闲着也是闲着。再说,对于某种语言的弱点和优势有一个量化的分析,
对于我们今后在做项目的时候面临工具选择也少许有一点指导意义。另外,觉得好玩才
是我做这件事情的真正原因。

  选题

  选择一个什么样的程序问题进行这样的测试呢?这是一个很关键的问题,也最容易
影响测试的公平性。另外的,对于每种语言,各自的优势都是不同的。程序员的偏爱也
是各不相同的。在网上和现实中,对于什么语言更好一些的争论从来就没有停止过。甚
至的,各门各派的程序员所构成的各种阵营,把某种语言奉若神明的也不在少数。不信,
你在CSDN的JAVA论坛说一句"JAVA执行效率太低了云云"试试?立刻会被铺天盖地的板砖
掀翻在地。类似的,还有管理员对于操作系统的偏好和争论: 在Linux论坛你要是表扬
Windows, 其惨烈程度简直是难以言状。因此,从这个意义上来说,程序员们对于编程
语言的偏好,类似于战士之喜爱枪械,赛手之喜爱赛车,已经上升为一种精神层面的东
西了。蔡学镛先生说得好:有人逢微软必反,有人逢微软必捧。这是一种纯粹的精神上
的爱,但它可能会影响正常的、科学的思考。

  可以预料的,我这篇文章一定会遭到各路豪杰的迎头痛击。

  好了,让我们言归正转吧。首先的,我们的选题中要使用的各种程序语言的最常用
的要素。什么是最常用的要素呢?当然了,大家都有的就是赋值、数组操作、循环、判
断等。另外,对IO的操作也是编程语言重要的内容。其次的,操作时间一定要长,否则,
对于解释性的语言来说是极不公平的:解释器还没调入内存呢,人家编译派的已经运行
完了。最后,就是程序不能太复杂。除了我没有那么大的毅力用各种语言完成一个复杂
算法的决心外,程序过于复杂,算法在测试中起的作用就越来越大,影响运行效率的原
因也就增加了。算法过于复杂,开发工具的扩展部分用得也就越多。于是就成了语言附
加库之间的竞赛了,这是我不愿意看到的。

  考虑上述因素,我设计了一个简单的选题:从指定文本文件中搜索指定字符串,计
算个数。并且打印出搜索到的个数作为结果输出。作为程序员的你粗粗过一下脑子,马
上会想到这个算法里面包含了条件判断、循环、数组操作等基本的程序语言因素。这满
足了上面第一个条件。另外的,为了满足第二个条件,我准备了一个多达2G的文本文件,
总共有文本1500万行多。这保怔了足够的运行时间(但应该不会太长),而决不会一眨
眼就执行完了。最后的,我们都知道,在文本串里面搜索子串的算法是数据结构课本中
的一个典型的例子(考试也经常被考到的),也满足算法简单的要求。同时,为了让每
个程序的环境都一样,我得每测试一次就重新启动一次机器,避免CACHE的影响。

  准备

  比赛嘛,就需要公平。首先的,硬件平台要统一。我找了一台看起来还不错的机器
(服务器):两颗PIII800,1G内存。 操作系统嘛, 原来的机器上有新装的Windows2000
Server版本。几乎没装什么别的应用。我偷懒了一下,没有重新安装OS,就这样用吧。

  第一个选手:PERL

  如果别人交给我这个题目,我会马上决定用PERL语言来做这件事。这个题目是完全
的文本处理问题,还有比用PERL来做更合适的吗?因为PERL是专门为了文本处理而编制
的语言。事实上也是这样,我用了2分钟,写了几行代码,就轻松实现了这个问题。 这
也说明了,选择适用的编程语言工具,比选择喜爱的工具更重要。

#!/usr/bin/perl
$filename="d:/access.log_";
$count = 0;
open(FILE , "<$filename");
while(<FILE>)
{
  @match_list = ($_ =~ /HIT/g);
  $count=$count+@match_list;
}
close(FILE);
print "Count = $count ";
exit

  PERL是一位语言学家Larry Wall发明的,事实上,早期这种语言是专门用于在UNIX
平台处理文字文件的(Perl=Practical Extraction Report Language: 实用报表析取语
言)。后来人们发现有大量文本构成的HTML页面用PERL来做CGI程序生成动态页面再合适
不过了。因为互联网的兴起,PERL跟着发大了起来。 这种语言的语法和C语言基本类似,
因此比较好掌握,并且的,其关于"正则表达式"处理的强大功能目前基本上无人能够望
其项背。 事实上,类似于"过滤出含有TOM或者ABC的、并且后者的第一个和第三个字母
大写,前者最少出现2次,后者出现5次、而且中间间隔8个或4个字母或空格的文本行"。
我猜你正在反复的揣摩这句话,事实上,这就是所谓正则表达式,这样的问题,在PERL
只需要一行语句就可以完成。在C语言中需要多少语句才能实现呢。

  我略略解释一下上面的程序,让没有用过PERL语言的程序员也有个感性认识。

  第一行是在UNIX中才用得到,因为PERL是一种基于解释的脚本语言。

  第四行是打开文件

  下面的循环是一行一行的读文件的内容。循环中间的第一句话是把凡是文本行中含
有的HIT全部放到一个数组中;循环中中的第二句话是统计一下刚才的数组中有几个HIT,
然后累加起来。循环完成了,我们的任务也就完成了。怎么样,很简单吧?"/HIT/g"就
是最简单的正则表达式。

  现在的PERL语言早已经不是原来的脚本语言形象了,现代PERL几乎具备了其特语言
的所有特性,并且的在模块的功能帮助下,可以实现很大的应用。而且还增加了一些面
向对象的特点。尽管大多数人仍然在用它处理大量的文本,但也有使用PERL完成大型应
用的,尤其是在WEB方面。值得一提的是PERL也是一个跨平台语言。

  我的这个程序在测试平台上,使用PERL5.8解释器,用了8分18秒08完成了1500万行
文本的扫描,并得出了正确的结果。

  第二个选手:纯C

  也许年龄大了,但是我真的很喜欢C语言。 而且我最喜欢的就是使用指针和强制类
型转换来任意操作数据。我甚至会在程序里通过指针手工拼凑一个长整性的数据。说句
可能引起争议的话,我觉得JAVA语言抛弃可爱的指针的做法基本上就是逃避。因为掌握
不好就不用,到头来就是牺牲了效率。

  本文这个题目,用C语言来实现应该还是比较不错的选择。 下面的代码就是在VC下
面实现的纯C代码的字符串搜索程序(为了避免图形界面的干扰,一律做成控制台程序)。
编译的时候使用速度优先编译选项。

#include <stdio.h>
#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 ",&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 ",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写成的, 尽管很复杂,但很
少有使用面向对象技术的。为什么,不是面向对象技术不好,也不是操作系统核心不够
复杂(那什么复杂?),主要的考虑就是效率问题。

#include <stdio.h>
#include <string.h>
#include <fstream.h>

void main()
{
  int len=2048;
  char filename[20];//文件名
  char buff[10000];//文件缓冲区
  char hit[5];
  int i,j,flag=0;
  int max;
  int count=0;//最后的结果
  strcpy(&filename[0] , "d:/access.log_");
  strcpy(&hit[0] , "HIT");
  buff[0]=0x0;
  buff[1]=0x0;
  //用输入流打开文件:
  ifstream input(&filename[0]);
  //读取文件内容
  while(input)
  {
    input.getline(&buff[2] , len);
    max = strlen(&buff[2]);
    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);
        }
      }
    }
  }
  printf("count:%d ",count);
}

  这段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的文件流类, 运行了循环、 条件判断、数组操作等基本的语言因素。环境是J2SE
1.3.1-06。JAVA程序做1500万行的文本扫描用了8分21秒18。 应该说是几种语言中最慢
的,基本上和纯解释的PERL是在同一水准。J2EE的JVM环境还是经过优化的所谓HOTSPOT。

import java.io.*;
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++等其他的语言虽然用了非常方便的流按行读出,但是多做了很多事情:
每一个字符都要判断其是不是回车换行符,而按行读近来,每次缓冲的也要少很多。因
此其他几种语言就大大的吃亏了。不过这并不影响结论性的东西,因为测试本身就说明
越方便就效率越低。事情总是要有人做,不是吗?
智能推荐

注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
© 2014-2019 ITdaan.com 粤ICP备14056181号  

赞助商广告