前言
這系列文章主要是在研究如何增進檔案寫入之速度,若寫檔之次數、大小均沒一定數量時,這系列文章並不會引起興趣,可跳過。但通常讀取次數增多時 (通常 > 50 MB),便會感覺用 fscanf 速度不夠快,於是有要加速之需求,此系列文章便是在探討是否有其它方法使讀寫檔案方式能增快。
本系列文章只探討數值之讀寫,避開了字串之讀寫,若檔案本身單純只為字串所組成,此系列文章並不適用;若檔案本身單純為數值 (整數及浮點數) 所組成,這系列文章便可適用;若字串、數值為參半,仍可做為相當之參考。
測試時,測試次數 (TIMES) 均設為 二千萬 次,測試方法為調用 rand() 並寫入檔案,也由於為確保每份code拿到之資料均為相同,於此便不再調用 srand(time(NULL)); 謹慎之人可直接再調用 srand(0); 便更能確保每份寫入之檔案資料均為相同。而時間測試方面,由於執行之時間均有一定耗時,故謹採用 clock() 函式,也降低對系統之負擔。
測試環境
由於測時與硬體設備和 compiler 優化與設定有極大關係,測試結果謹供參考,但相對之效能 ( 如 A 方法為 B 方法耗時之二倍) 應可做為相當之參照。於此附上吾人手邊之開發環境
作業系統:XP
Compiler:VC 2008
CPU :AMD Athlon(tm) II X2 245, 2.91GHz
RAM :2.75GB 實體延伸
測試結果
請看圖說故事。
產生測試資料
為避免由於資料來源之不一,故先寫一段程式產生統一之測試資料,測試資料乃調用rand 進行,同時生成檔案分整數與浮點數部份,生成之數字共 200M 個,檔案大小均超過 1GB,耗時約 20 分鐘(floating 計算最為耗時,未經優化),原始碼如下
#include <stdio.h> #include <stdlib.h> #define N 200000000 #define NEW_LINE_N 20 #define FILE_INT "data_integer.txt" #define FILE_FLOATING "data_floating.txt" int main() { const int new_line_n = NEW_LINE_N-1; int i=0; FILE *fp = fopen(FILE_INT, "w"); srand(0); for(i=0; i!=N; ++i) { fprintf(fp,"%d ", rand()); if(i%NEW_LINE_N==new_line_n) fputc('\n', fp); } fclose(fp); srand(0); fp=fopen(FILE_FLOATING, "w"); for(i=0; i!=N; ++i){ fprintf(fp, "%lf ", rand() + rand()/(double)RAND_MAX); if(i%NEW_LINE_N==new_line_n) fputc('\n', fp); } fclose(fp); return 0; }
1. fscanf
這是最常見、最方便的方法,但卻是最慢的方法。
fp=fopen(FILE_INT, "r"); while(fscanf(fp, "%d",&integer)!=EOF) /* printf("%d\n", integer); */; fclose(fp); fp=fopen(FILE_FLOATING, "r"); while(fscanf(fp, "%lf", &floating)!=EOF) /* printf("%lf\n", flating); */; fclose(fp);
2. fgets + strtok + atoi(atof)
這是最基本、簡單的加速法,約可節省 fscanf 23~28% 之時間。
fp = fopen(FILE_INT,"r"); while(fgets(buffer, BUF_SIZE, fp)!=NULL) { ptr=strtok(buffer, " "), integer = atoi(ptr); while((ptr=strtok(NULL, " \n"))!=NULL ){ integer=atoi(ptr); /* printf("%d\n", integer); */ } } fclose(fp); fp = fopen(FILE_FLOATING,"r"); while(fgets(buffer, BUF_SIZE, fp)!=NULL) { ptr=strtok(buffer, " \n"), floating = atof(ptr); while((ptr=strtok(NULL, " \n"))!=NULL ){ floating=atof(ptr); /* printf("%d\n", floating); */ } } fclose(fp);
3. fgets + strchr + atoi(atof)
這方法與 strtok 相似,但處理起來較為麻煩點,但處理之時間比 strtok 快!約可節省 fscanf 33%~48% 之時間。
fp = fopen(FILE_INT,"r"); while(fgets(buffer, BUF_SIZE, fp)!=NULL) { ptr = buffer; integer = atoi(ptr); while( (ptr = strchr(ptr, ' '))!=NULL && *(ptr+1)!='\n') { integer = atoi(ptr++); /* printf("%d", integer); */ } } fclose(fp); fp = fopen(FILE_FLOATING,"r"); while(fgets(buffer, BUF_SIZE, fp)!=NULL) { ptr = buffer, floating = atof(ptr); printf("%lf ", floating); while( (ptr=strchr(ptr, ' '))!=NULL && *(ptr+1)!='\n') { floating = atof(ptr++); /* printf("%lf ", floating); */ } } fclose(fp);
4. fgetc
這方法效果也很好,但若是浮點數在實做上有一定難度,約可節省 fscanf 33%~45% 之時間。首先要有一個 flag - is_integer 判斷目前之數字是否為整數部份;is_integer = 1代表為整數部份;is_integer=0 代表該數字為小數部份。步驟大致如下
(0) 初始化設定:is_integer = 1, integer=0 (整數部份), floating=0 (小數部吩), power=0.1 (乘幕部份)
說明:一開始讀到數字的話都是整數部份(is_integer=1),直到讀到小數點才切到小數部份(is_integer=0)
(1) 讀到字元為 '\n' : 直接跳過不處理(continue)
說明:若不處理的話,每次換行會誤判一次 0
(2) 若讀到的是字元 且 is_integer=1,integer = integer * 10 + (ch & 0x0f);
說明:抓取整數部份
(3) 若讀到的是字元 且 is_integer=0,floating=(ch & 0x0f)*power, power*=0.1;
說明:抓取小數部份
(4) 若讀到的是小數點 ".",則將 is_integer 設成0
說明:從「整數部份」切換至「小數部份」
(5) 若讀到的分隔符號 (此處為空白鍵 " "),則視為這段資料結束,將 floating+=integer
說明:進行 floating 輸出,同時再次進行初始化。
fp = fopen(FILE_INT, "r"); integer=0; while( (ch=fgetc(fp)) !=EOF){ if(ch=='\n') integer=0; else if(isdigit(ch)) integer = integer*10 + (ch & 0x0f); else if(ch==' ') /*printf("%d ", integer),*/ integer=0; else NULL; } fclose(fp); fp = fopen(FILE_FLOATING, "r"); integer=0, floating=0, is_integer=1, power=0.1; while( (ch=fgetc(fp)) !=EOF){ if(ch=='\n') continue; else if(isdigit(ch) && is_integer) { integer = integer*10 + (ch & 0x0f); } else if(isdigit(ch) && !is_integer) { floating+= (ch & 0x0f)*power, power*=0.1; } else if(ch=='.') { is_integer=0; } else if(ch==' ') { floating+=integer; /*printf("%lf ",floating); */ integer=0, floating=0, is_integer=1, power=0.1; } else NULL; } fclose(fp);
最後聲明,如果真的是大檔案要多次進行讀取,建議先轉成 bin file, 以便日後做 seek/read 動作,速度會快很多!
留言列表