前言

這系列文章主要是在研究如何增進檔案寫入之速度,若寫檔之次數、大小均沒一定數量時,這系列文章並不會引起興趣,可跳過。但通常寫入次數增多時 (通常 > 50 MB),便會感覺用 fprintf 速度不夠快,於是有要加速之需求,此系列文章便是在探討是否有其它方法使讀寫檔案方式能增快。

本系列文章只探討數值之讀寫,避開了字串之讀寫,若檔案本身單純只為字串所組成,此系列文章並不適用;若檔案本身單純為數值 (整數及浮點數) 所組成,這系列文章便可適用;若字串、數值為參半,仍可做為相當之參考。

測試時,測試次數 (TIMES) 均設為 二千萬 次,測試方法為調用 rand() 並寫入檔案,也由於為確保每份code拿到之資料均為相同,於此便不再調用 srand(time(NULL)); 謹慎之人可直接再調用 srand(0); 便更能確保每份寫入之檔案資料均為相同。而時間測試方面,由於測試時間多為 1~15 secs,故謹採用 clock() 函式,也降低對系統之負擔

 


 

測試環境

由於測時與硬體設備和 compiler 優化與設定有極大關係,測試結果謹供參考,但相對之效能 ( 如 A 方法為 B 方法耗時之二倍) 應可做為相當之參照。於此附上吾人手邊之開發環境

作業系統:XP
Compiler:VC 2008
CPU      :AMD Athlon(tm) II X2 245, 2.91GHz
RAM      :2.75GB 實體延伸


1. fprintf (110 MB 13.202 secs)
使用 fprintf 為最直覺、簡易之方式,但由於寫入資料量一大時,速度便拖慢,吾人用手邊電腦寫為二億次隨機亂數,共費時近 150 secs,2分半鐘,於是便開始有人討論如何增快檔案寫入速度之議題。

2. sprintf + fprintf + memcpy (110 MB 14.304 secs)
此法「理論上」似乎應為較快些。這方法主要是將每次要寫入之資料用 sprintf 寫入一 buffer 內,等 buffer 長度(通常設2000或 4000)快滿時,再一次寫入檔案之中。這種做法明顯是利用在 memory 之讀寫速度遠比硬碟之讀寫速度還快之特性,雖「謠言」指出這種方式絕對會較快,但吾人實測結果卻是比單純用 fprintf 還要慢 7 % 左右。初步判定應為在 sprintf 及 memcpy 上花費太多時間 (特別是 sprintf)。若對於此部份之程式碼認為可繼續增加改善,甚至可改得比 fprintf 還快,請不吝指教。

3. fwrite (76MB 2.376 secs)
接下來這二個方法事實上和上述之二個方法並不相同,因輸出之資料並非為文本模式,亦即,無法使用記事本直接開啟,此仍其缺點。但優點為速度快 (為 fprintf 之 7~8 倍),且在讀檔時可進行隨機讀檔。如,假設只要讀取第 1000 筆資料,可直接用 fsetpos 移到第 1000 筆資料直接讀出;但若用 fscanf 方式,則需先讀走前面 999 筆,方可再讀取第 1000 筆資料。

4. fwrite + array (76MB 0.768 secs)
這部份事實上便是 fwrite 之加強版。上述之 fwrite 是一次寫入一個數值進去,但事實上可以把資料先都丟到一個 array 裡面,待 array 滿之後,再一次寫入檔案之中。值得注意的是,array size 設到某個上限後,再設大效能並不會有所明顯增加。以此例,array size 設 100 與設 10000,實際上只增進 1ms (當然也不排除為計時誤差)。

結論是,用 fwrite + array 較快,若此份檔案並非予以他人參閱,直接用 fwrite 方式較為佳。但於程式碼中必須為該檔定義其欄位。

以下即為各方法之核心程式碼


1. fprintf (110 MB 13.202 secs)

t1 = clock();
for(i=0; i!=TIMES; ++i){
    fprintf(fp,"%d ", rand());
}
t2 = clock();

 

2. sprintf + fprintf + memcpy (11 MB 14.304 secs)

t1 = clock();
for(i=0; i!=TIMES; ++i){
    /* sprintf 將傳回 tmp 字串結果之長度 */
    len_tmp = sprintf(tmp, "%d ", rand()); 

    /* buffer 已放不下, 將 buffer 輸出清空, 再將 tmp 丟給 buffer*/
    if(len_tmp+len_buf >= BUF_SIZE){ 
       fprintf(fp, "%s", buffer); /* 先將 buffer 輸出 */
       memcpy(buffer, tmp, len_tmp); /* 再將 tmp 給 buffer,以 memcpy 取代 strcpy */
       len_buf = len_tmp; /* 更新 buffer 長度紀錄 */
    }
    else{ /* buffer 還放得下 */
       memcpy(buffer+len_buf, tmp, len_tmp); /* 使用 memcpy 取代 strcat */
       len_buf+= len_tmp; /* 更新 buffer 長度紀錄 */
    }
}
/* 結束時,將剩下的 buffer 一次輸出 */
fprintf(fp, "%s", buffer);
t2 = clock();

 

3. fwrite (76MB 2.376 secs)

t1 = clock();
for(i=0; i!=TIMES; ++i){
    x = rand();
    fwrite(&x, sizeof(int), 1, fp);
}
t2 = clock();

 

 4. fwrite + array (76MB 0.768 secs)

#define ARRAY_SIZE 10000
int i, counter=0
t1 = clock();
for(i=0; i!=TIMES; ++i){
    x[counter++] = rand();
    if(counter==ARRAY_SIZE){
       fwrite(x, sizeof(int), ARRAY_SIZE, fp);
       counter=0;
    }
}
t2 = clock();

 


 

在寫檔時,速度可探討的方法不多;但在讀檔時,可探討的方法有許多。吾人實做過近 10 種讀取數值之方式,最好之方法與最差之方法效能相差2倍!如果檔案單純為數值,檔案很大,且使用的方法為 fscanf,那代表該換一種方法了 - fscanf 讀數值是最慢的方法

edisonx 發表在 痞客邦 PIXNET 留言(5) 人氣()


留言列表 (5)

發表留言
  • jeff
  • 請問 文章中 "但於程式碼中必須為該檔定義其欄位。" 是指 fp的宣告嗎?
    可否列出fp的參考方式, 謝謝

  • 不是。指的是二進位檔案內容的順序,你可以閱讀 bmp 檔案格式,就是典型的範例 ( 較簡單 ) ,幾乎所有的檔案格式都會有所定義。fp 宣告: FILE * fp ; 其他的多去找書。

    edisonx 於 2013/11/10 12:43 回覆

  • jeff
  • 格主您好:

    以下是我的test code, 我想把1 2 3(中間空白隔開)寫入檔案中 , 我是知道如何寫入結構及字串 , 但如何將整數陣列整個寫入, 各數值間還需隔開 , 不曉得您可否給一點提示. 打擾之處請見諒.
    -----------------
    FILE *fp;
    fp =fopen("s1.dat","a");
    int p[50];
    p[0]=1;
    p[1]=3;
    p[2]=5;
    fwrite(p,sizeof(int),3,fp);
  • 可能你不太清楚,fwrite , fread 大多情況是用 "wb" , "rb" 方式寫,所以較沒辦法用你講的方式去做。也就是寫入的結果,用檔案開啟會較不直覺。

    int p[3] = {1,2,3};
    FILE * fp = fopen("s1.dat", "wb"); ///< 寫檔
    fwrite(p, sizeof(int), 3, fp);
    fclose(fp);

    到上面其實你用記事本開 s1.data 是看不出什麼東西的,除非用 16 進位軟體才看得懂。若要讀出來的話,就變成

    int rst[3] = {0};
    FILE * fout = fopen("s1.dat", "rb");
    fread(rst, sizeof(int), 3, fout); ///< 這裡就讀出來了
    fclose(fout);

    換句話說,如果要速度快的話,通常寫入的檔案不會很直接的讓使用者打開就看得到,必須配合看16進位的軟體做輔助 (也不是一般使用者會用),如 PSPad, UltraEditor 等軟體。

    如果你真的是要如 1 2 3 中間用空白隔開寫入檔案,你可以考慮,用 sprintf 到一個 buffer 裡去,再用 fwrite 一次把 buffer 寫到檔案裡去,但這速度不一定會比 fprintf 還快,一份網友測試(抱歉我還沒時間測過)的數據是,當要寫入的數值是 "浮點數" 時,用 sprintf + fwrite 會比較慢。

    下面的 code 是上述的簡略 implement, 沒考慮 buffer overflow, 有興趣可自行再考慮。

    #define BUF_SIZE 2000
    char buf[BUF_SIZE];
    char * ptr;
    int arr[10] = {1,2,3,4,5,6,7,8,9,0};
    int nLen = 0 ;

    FILE * fout = fopen("s1.data", "wb");

    // 一次將要寫的東西都寫到 buf 裡去
    ptr = buf;
    for(int i = 0 ; i < 10 ; ++i){
    nLen = sprintf(ptr, "%d ", arr[i]);
    ptr += nLen;
    }
    *ptr++ = '\0'; ///< 放入字串結束位元
    fwrite(buf, ptr, 1, ptr - buf, fout); ///< 一次寫入檔案
    fclose(fp);

    其它函式或指標用法不熟的話,再請教周圍的朋友幫忙,暫時沒太多時間寫一篇文章。

    edisonx 於 2013/11/10 16:23 回覆

  • jeff
  • 謝謝格主熱心且完整的回覆 , 原本我不了解fwrite及fread只能處理binary file , 現在我知道如何處理了 , 謝謝.
  • tim
  • int ss = 640*480 ;
    Byte ddd[ 640*480 ] ;
    int s = clock();

    for( int i= 0 ; i< 200 ; i++ )
    fwrite( ptr , sizeof(Byte) , ss , h ) ;

    int e = clock();
    this->Caption = e-s ;
    fclose(h);
    不好意思~請問一下我這樣跑出來60,000 KB 要8秒可能問題出在哪邊
    我是C++BUILDER
  • 你問倒我了,我沒用過 BCB。換 compiler 再試試結果會不會一樣。

    edisonx 於 2014/06/12 22:41 回覆

  • 悄悄話

您尚未登入,將以訪客身份留言。亦可以上方服務帳號登入留言

請輸入暱稱 ( 最多顯示 6 個中文字元 )

請輸入標題 ( 最多顯示 9 個中文字元 )

請輸入內容 ( 最多 140 個中文字元 )

請輸入左方認證碼:

看不懂,換張圖

請輸入驗證碼