事發
我無聊的翻著散落案頭的書籍,這些都是五花八門的關於編程和系統管理的著作。幹了這麼多年程式員,大大小小的軟體和項目也做了無數。每每有新入行的朋友問我這個所謂的"老前輩":哪種語言最好之類的問題,我總會作出一副知識淵博的樣子,複述著從更老的老前輩那裡聽來的或者某些名著上看來的"知識"。就好比我們從學習編程的第一天起,就被電腦老師告知,COBOL語言是擅長處理商務事務、FOTRAN語言是用於科學計算一樣。類似的知識還有"組合語言比C語言快得多"以及"JAVA是一種效率很低的語言環境"在一代又一代的程式員中口耳相傳,幾乎成為了毋庸置疑的真理。
我產生了一個想法,能不能對於同一個應用用幾種程式設計語言分別實現,來比較一下看看到底哪種語言效率最高?
老實說我自己都覺得這個想法很無聊,想想誰會反覆用不同的語言寫同一個程式呢?下雨天打孩子,閑著也是閑著。再說,對於某種語言的弱點和優勢有一個量化的分析,對於我們今後在做項目的時候面臨工具選擇也少許有一點指導意義。另外,覺得好玩才是我做這件事情的真正原因。
選題
選擇一個什麼樣的程式問題進行這樣的測試呢?這是一個很關鍵的問題,也最容易影響測試的公平性。另外的,對於每種語言,各自的優勢都是不同的。程式員的偏愛也是各不相同的。在網上和現實中,對於什麼語言更好一些的爭論從來就沒有停止過。甚至的,各門各派的程式員所構成的各種陣營,把某種語言奉若神明的也不在少數。不信,你在CSDN的JAVA論壇說一句"JAVA執行效率太低了云云"試試?立刻會被鋪天蓋地的板磚掀翻在地。類似的,還有管理員對於作業系統的偏好和爭論:在Linux論壇你要是表揚Windows,其慘烈程度簡直是難以言狀。因此,從這個意義上來說,程式員們對於程式設計語言的偏好,類似於戰士之喜愛槍械,賽手之喜愛賽車,已經上升為一種精神層面的東西了。蔡學鏞先生說得好:有人逢微軟必反,有人逢微軟必捧。這是一種純粹的精神上的愛,但它可能會影響正常的、科學的思考。
可以預料的,我這篇文章一定會遭到各路豪傑的迎頭痛擊。
好了,讓我們言歸正轉吧。首先的,我們的選題中要使用的各種程式語言的最常用的要素。什麼是最常用的要素呢?當然了,大家都有的就是賦值、數組操作、迴圈、判斷等。另外,對IO的操作也是程式設計語言重要的內容。其次的,操作時間一定要長,否則,對於解釋性的語言來說是極不公平的:解譯器還沒調入記憶體呢,人家編譯派的已經運行完了。最後,就是程式不能太複雜。除了我沒有那麼大的毅力用各種語言完成一個複雜演算法的決心外,程式過於複雜,演算法在測試中起的作用就越來越大,影響運行效率的原因也就增加了。演算法過於複雜,開發工具的擴充部分用得也就越多。於是就成了語言附加庫之間的競賽了,這是我不願意看到的。
考慮上述因素,我設計了一個簡單的選題:從指定文字檔中搜尋指定字串,計算個數。並且列印出搜尋到的個數作為結果輸出。作為程式員的你粗粗過一下腦子,馬上會想到這個演算法裡麵包含了條件判斷、迴圈、數組操作等基本的程式語言因素。這滿足了上面第一個條件。另外的,為了滿足第二個條件,我準備了一個多達2G的文字檔,總共有文本1500萬行多。這保怔了足夠的已耗用時間(但應該不會太長),而決不會一眨眼就執行完了。最後的,我們都知道,在文本串裡面搜尋子串的演算法是資料結構課本中的一個典型的例子(考試也經常被考到的),也滿足演算法簡單的要求。同時,為了讓每個程式的環境都一樣,我得每測試一次就重新啟動一次機器,避免CACHE的影響。
準備
比賽嘛,就需要公平。首先的,硬體平台要統一。我找了一台看起來還不錯的機器(伺服器):兩顆PIII800,1G記憶體。作業系統嘛,原來的機器上有新裝的Windows2000Server版本。幾乎沒裝什麼別的應用。我偷懶了一下,沒有重新安裝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語言基本類似,因此比較好掌握,並且的,其關於"Regex"處理的強大功能目前基本上無人能夠望其項背。事實上,類似於"過濾出含有TOM或者ABC的、並且後者的第一個和第三個字母大寫,前者最少出現2次,後者出現5次、而且中間間隔8個或4個字母或空格的文本行"。我猜你正在反覆的揣摩這句話,事實上,這就是所謂Regex,這樣的問題,在PERL只需要一行語句就可以完成。在C語言中需要多少語句才能實現呢。
我略略解釋一下上面的程式,讓沒有用過PERL語言的程式員也有個感性認識。
第一行是在UNIX中才用得到,因為PERL是一種基於解釋的指令碼語言。
第四行是開啟檔案
下面的迴圈是一行一行的讀檔案的內容。迴圈中間的第一句話是把凡是文本行中含有的HIT全部放到一個數組中;迴圈中中的第二句話是統計一下剛才的數組中有幾個HIT,然後累加起來。迴圈完成了,我們的任務也就完成了。怎麼樣,很簡單吧?"/HIT/g"就是最簡單的Regex。
現在的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
;My Code開始:
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。
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;//產生一個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++等其他的語言雖然用了非常方便的流按行讀出,但是多做了很多事情:每一個字元都要判斷其是不是斷行符號分行符號,而按行讀近來,每次緩衝的也要少很多。因此其他幾種語言就大大的吃虧了。不過這並不影響結論性的東西,因為測試本身就說明越方便就效率越低。事情總是要有人做,不是嗎?