C大部分讀取文件的時候采用fgetc, 最近在使用過程中發現性能不是很理想.都懂得fgetc每次只能讀取一個字符, IO操作太頻繁.
所以性能低. 本文希望通過標准庫函數fread 函數構建讀取緩沖區來優化這個瓶頸.
在正式開始實驗總結之前, 傳一個VS C/C++ 開發的技巧給大家, 天外飛仙~ .
M$忽略C++太久了,對於C直接放棄, 在其Visual Studio IDE中. 但是吧在Window 還是它的IDE寫C 系列語言最爽.
現在很流行一個低端套路是, Window VS 開發, Linux上部署. 而我們的問題就是關於這個, 這種不同平台的開發和部署,
存在一個坑就是編碼問題. 而這裡就是希望完美的解決這個編碼問題.
解決基准是選用UTF-8 帶簽名的編碼, VS , GCC都能編譯通過. 那就一言不合上圖了, 首先定位VC模板文件
將上面模板文件備份一份, 復制一份, 用VS打開復制的那份
高級選項另存為上面編碼格式. 最終保存替換原先的模板文件. 從此以後, 編碼問題 perfect! 繼續扯一點, 隨著寫代碼時間增長, VS的依賴已經不重要的.
可惜喜歡打游戲, 還是被window 游戲機綁定了. 真的是離開I可以, 請付出代價~ /(ㄒoㄒ)/~~
這裡驗證的是fgetc 和 fread讀取性能的對比. 在說之前, 先介紹個測試宏
// 簡單的time幫助宏 #ifndef TIME_PRINT #define _STR_TIME_PRINT "The current code block running time:%lf seconds\n" #define TIME_PRINT(code) \ do {\ clock_t __st, __et;\ __st = clock();\ code\ __et = clock();\ printf(_STR_TIME_PRINT, (0.0 + __et - __st) / CLOCKS_PER_SEC);\ } while(0) #endif // !TIME_PRINT
非常還用, 將代碼塊插入到 code中, 就可以使用了. 那繼續了, 第一個測試的主體內容是, 實驗一 fread對比fgetc
#include <stdio.h> #include <time.h> #include <stdlib.h> #define _STR_DATA "data.txt" #define _INT_DATA (1024*1024*32) // 測試 fgetc 性能 void test_fgetc(void); // 測試 fread 性能 void test_fread(void); // // 測試C大文件處理方式 // int main(int argc, char * argv[]) { // 先構建測試環境 FILE * txt = fopen(_STR_DATA, "r"); if (NULL == txt) { txt = fopen(_STR_DATA, "w"); if (NULL == txt) { fprintf(stderr, "main fopen w " _STR_DATA " error!\n"); exit(EXIT_FAILURE); } // 開始寫入數據 for (int i = 0; i < _INT_DATA; ++i) fprintf(txt, "%d", i); } fclose(txt); // 開始測試數據, 分批測試 TIME_PRINT({ test_fgetc(); }); TIME_PRINT({ test_fread(); }); return 0; }
其中兩個測試函數如下.
// // 測試 fgetc 性能 // void test_fgetc(void) { FILE * txt = fopen(_STR_DATA, "r"); if (NULL == txt) { fprintf(stderr, "test_fgetc fopen w " _STR_DATA " error!\n"); return; } size_t cnt = 0; int c; while ((c = fgetc(txt)) != EOF) ++cnt; fclose(txt); printf("test_fgetc cnt = %d\n", cnt); } // // 測試 fread 性能 // void test_fread(void) { FILE * txt = fopen(_STR_DATA, "r"); if (NULL == txt) { fprintf(stderr, "test_fread fopen w " _STR_DATA " error!\n"); return; } size_t cnt = 0; char buf[BUFSIZ]; for (;;) { int rn = fread(buf, sizeof(char), BUFSIZ, txt); // 存在信號中斷情況, 不考慮 cnt += rn; if (rn < BUFSIZ) break; } fclose(txt); printf("test_fread cnt = %d\n", cnt); }
測試主要思路是.
a. 構建差不多是200-300Mb的數據文件
b. 通過fgetc 完畢, 輸出時間
c. 通過fread 完畢, 輸出時間
測試結果如下
我們發現, fread構建緩沖區有質的飛躍.
附錄測試完整內容 file_test.c
#include <stdio.h> #include <time.h> #include <stdlib.h> #define _STR_DATA "data.txt" #define _INT_DATA (1024*1024*32) // 簡單的time幫助宏 #ifndef TIME_PRINT #define _STR_TIME_PRINT "The current code block running time:%lf seconds\n" #define TIME_PRINT(code) \ do {\ clock_t __st, __et;\ __st = clock();\ code\ __et = clock();\ printf(_STR_TIME_PRINT, (0.0 + __et - __st) / CLOCKS_PER_SEC);\ } while(0) #endif // !TIME_PRINT // 測試 fgetc 性能 void test_fgetc(void); // 測試 fread 性能 void test_fread(void); // // 測試C大文件處理方式 // int main(int argc, char * argv[]) { // 先構建測試環境 FILE * txt = fopen(_STR_DATA, "r"); if (NULL == txt) { txt = fopen(_STR_DATA, "w"); if (NULL == txt) { fprintf(stderr, "main fopen w " _STR_DATA " error!\n"); exit(EXIT_FAILURE); } // 開始寫入數據 for (int i = 0; i < _INT_DATA; ++i) fprintf(txt, "%d", i); } fclose(txt); // 開始測試數據, 分批測試 TIME_PRINT({ test_fgetc(); }); TIME_PRINT({ test_fread(); }); return 0; } // // 測試 fgetc 性能 // void test_fgetc(void) { FILE * txt = fopen(_STR_DATA, "r"); if (NULL == txt) { fprintf(stderr, "test_fgetc fopen w " _STR_DATA " error!\n"); return; } size_t cnt = 0; int c; while ((c = fgetc(txt)) != EOF) ++cnt; fclose(txt); printf("test_fgetc cnt = %d\n", cnt); } // // 測試 fread 性能 // void test_fread(void) { FILE * txt = fopen(_STR_DATA, "r"); if (NULL == txt) { fprintf(stderr, "test_fread fopen w " _STR_DATA " error!\n"); return; } size_t cnt = 0; char buf[BUFSIZ]; for (;;) { int rn = fread(buf, sizeof(char), BUFSIZ, txt); // 存在信號中斷情況, 不考慮 cnt += rn; if (rn < BUFSIZ) break; } fclose(txt); printf("test_fread cnt = %d\n", cnt); }View Code
實驗二 fread最優解
這裡測試的主要思路是基於fread設置不同的緩沖區, 開始測試性能對比情況. 首先test_file_define.c
#include <stdio.h> #include <time.h> #include <stdlib.h> #define _STR_DATA "data.txt" #define _INT_DATA (1024*1024*128) #define _INT_SZS (64) #define _INT_SZE (4096) // 測試 fread 性能 void test_fread(int sz); // // 測試C大文件處理方式 // int main(int argc, char * argv[]) { // 先構建測試環境 FILE * txt = fopen(_STR_DATA, "r"); if (NULL == txt) { txt = fopen(_STR_DATA, "w"); if (NULL == txt) { fprintf(stderr, "main fopen w " _STR_DATA " error!\n"); exit(EXIT_FAILURE); } // 開始寫入數據 for (int i = 0; i < _INT_DATA; ++i) fprintf(txt, "%d", i); } fclose(txt); // 開始測試數據, 分批測試 for (int sz = _INT_SZS; sz <= _INT_SZE; sz <<= 1) { clock_t st, et; st = clock(); test_fread(sz); et = clock(); printf("sz = %6d => time:%lf seconds\n", sz, (0.0 + et - st) / CLOCKS_PER_SEC); } return 0; } // // 測試 fread 性能 // void test_fread(int sz) { FILE * txt = fopen(_STR_DATA, "r"); if (NULL == txt) { fprintf(stderr, "test_fread fopen w " _STR_DATA " error!\n"); return; } size_t cnt = 0; char buf[_INT_SZE]; for (;;) { int rn = fread(buf, sizeof(char), sz, txt); // 存在信號中斷情況, 不考慮 cnt += rn; if (rn < sz) break; } fclose(txt); printf("test_fread cnt = %d\n", cnt); }
最終測試結果如下
直接說我得到的結論是
1). fread讀取的時候, buf 和 文件大小, 機器等共同影響了最優解
2). 在設置緩沖大小為BUFSIZ左右, 性能都是可以接受的.
通過上面兩個實驗, 最終得到的一個結論. 處理大文件IO讀取時候, 設計緩沖區可以采用下面套路將會獲得更好的性能.
char buf[BUFSIZ]; size_t rn; do { rn = fread(buf, sizeof(char), BUFSIZ, txt); if (rn < 0) { // 初始失敗的情況 .... } // 處理合法情況 .... } while(rn == BUFSIZ);
到這裡, 通過上面結論, 開始構建一個成果, 例如讀取全部文件內容. 當然限定文件大小在100mb以內吧, 太大需要采用分量讀取算法了.
會使用到的輔助操作宏
// // 控制台輸出完整的消息提示信息, 其中fmt必須是 "" 包裹的字符串 // CERR -> 簡單的消息打印 // CERR_EXIT -> 輸出錯誤信息, 並推出當前進程 // CERR_IF -> if語句檢查, 如果符合標准錯誤直接退出 // #ifndef _H_CERR #define _H_CERR #define CERR(fmt, ...) \ fprintf(stderr, "[%s:%s:%d][errno %d:%s]" fmt "\n",\ __FILE__, __func__, __LINE__, errno, strerror(errno), ##__VA_ARGS__) #define CERR_EXIT(fmt,...) \ CERR(fmt, ##__VA_ARGS__), exit(EXIT_FAILURE) #define CERR_IF(code) \ if((code) < 0) \ CERR_EXIT(#code) #endif
首先聲明讀取文件的接口部分.
#ifndef _STRUCT_TSTR #define _STRUCT_TSTR struct tstr { char * str; // 字符串實際保存的內容 size_t len; // 當前字符串長度 size_t cap; // 字符池大小 }; // 定義的字符串類型 typedef struct tstr * tstr_t; #endif // !_STRUCT_TSTR // // 簡單的文件讀取類,會讀取完畢這個文件內容返回,失敗返回NULL. // path : 文件路徑 // return : 創建好的字符串內容, 返回NULL表示讀取失敗 // extern tstr_t tstr_freadend(const char * path);
這麼設計方面, 後續操作, 讀取內容長度, 繼續添加內容方便些. 詳細的實現如下
// // 簡單的文件讀取類,會讀取完畢這個文件內容返回,失敗返回NULL. // path : 文件路徑 // return : 創建好的字符串內容, 返回NULL表示讀取失敗 // tstr_t tstr_freadend(const char * path) { tstr_t tstr; char buf[BUFSIZ]; size_t rn; char * ctmp; FILE * txt = fopen(path, "r"); if (NULL == txt) { CERR("tstr_freadend fopen r %s is error!", path); return NULL; } // 分配內存 tstr = malloc(sizeof(struct tstr)); if (NULL == tstr) { fclose(txt); CERR("tstr_freadend malloc is error! path = %s.", path); return NULL; } tstr->len = 0; tstr->cap = _INT_TSTRING; tstr->str = NULL; // 讀取文件內容 do { rn = fread(buf, sizeof(char), BUFSIZ, txt); if (rn < 0) { CERR("tstr_freadend fread is error! path = %s. rn = %d.", path, rn); fclose(txt); free(tstr->str); free(tstr); return NULL; } // 構建數據 if (tstr->cap < tstr->len + rn) { do tstr->cap <<= 1; while (tstr->cap < tstr->len + rn); ctmp = realloc(tstr->str, tstr->cap); if (NULL == ctmp) { CERR("tstr_freadend realloc is error! path = %s!", path); fclose(txt); free(tstr->str); free(tstr); return NULL; } tstr->str = ctmp; } // 開始拷貝數據 memcpy(tstr->str + tstr->len, buf, rn); tstr->len += rn; } while (rn == BUFSIZ); fclose(txt); // 繼續構建數據, 最後一行補充一個\0 if (tstr->cap < tstr->len + 1) { do tstr->cap <<= 1; while (tstr->cap < tstr->len + 1); ctmp = realloc(tstr->str, tstr->cap); if (NULL == ctmp) { CERR("tstr_freadend realloc is end error! path = %s!", path); free(tstr->str); free(tstr); return NULL; } } tstr->str[tstr->len] = '\0'; tstr->len += 1; return tstr; }
最終測試文件 file_test_build.c
#include <stdio.h> #include <errno.h> #include <stdlib.h> #include <assert.h> #include <stdint.h> #include <stddef.h> #include <string.h> #ifndef _STRUCT_TSTR #define _STRUCT_TSTR struct tstr { char * str; // 字符串實際保存的內容 size_t len; // 當前字符串長度 size_t cap; // 字符池大小 }; // 定義的字符串類型 typedef struct tstr * tstr_t; #endif // !_STRUCT_TSTR // // 控制台輸出完整的消息提示信息, 其中fmt必須是 "" 包裹的字符串 // CERR -> 簡單的消息打印 // CERR_EXIT -> 輸出錯誤信息, 並推出當前進程 // CERR_IF -> if語句檢查, 如果符合標准錯誤直接退出 // #ifndef _H_CERR #define _H_CERR #define CERR(fmt, ...) \ fprintf(stderr, "[%s:%s:%d][errno %d:%s]" fmt "\n",\ __FILE__, __func__, __LINE__, errno, strerror(errno), ##__VA_ARGS__) #define CERR_EXIT(fmt,...) \ CERR(fmt, ##__VA_ARGS__), exit(EXIT_FAILURE) #define CERR_IF(code) \ if((code) < 0) \ CERR_EXIT(#code) #endif // // 簡單的文件讀取類,會讀取完畢這個文件內容返回,失敗返回NULL. // path : 文件路徑 // return : 創建好的字符串內容, 返回NULL表示讀取失敗 // extern tstr_t tstr_freadend(const char * path); // // 測試文件讀取, 推薦都是50mb以下文件好處理一點 // int main(int argc, char * argv[]) { const char * path = "顧城 - 沒有名字的詩歌.txt"; tstr_t str = tstr_freadend(path); if (NULL == str) CERR_EXIT("日狗嗎? 這都讀不出來`!"); // 這裡打出數據 printf("當前總字符數:%d, 當前容量:%d.\n\n", str->len, str->cap); puts(str->str); free(str->str); free(str); return 0; } // 文本字符串創建的初始化大小 #define _INT_TSTRING (32) // // 簡單的文件讀取類,會讀取完畢這個文件內容返回,失敗返回NULL. // path : 文件路徑 // return : 創建好的字符串內容, 返回NULL表示讀取失敗 // tstr_t tstr_freadend(const char * path) { tstr_t tstr; char buf[BUFSIZ]; size_t rn; char * ctmp; FILE * txt = fopen(path, "r"); if (NULL == txt) { CERR("tstr_freadend fopen r %s is error!", path); return NULL; } // 分配內存 tstr = malloc(sizeof(struct tstr)); if (NULL == tstr) { fclose(txt); CERR("tstr_freadend malloc is error! path = %s.", path); return NULL; } tstr->len = 0; tstr->cap = _INT_TSTRING; tstr->str = NULL; // 讀取文件內容 do { rn = fread(buf, sizeof(char), BUFSIZ, txt); if (rn < 0) { CERR("tstr_freadend fread is error! path = %s. rn = %d.", path, rn); fclose(txt); free(tstr->str); free(tstr); return NULL; } // 構建數據 if (tstr->cap < tstr->len + rn) { do tstr->cap <<= 1; while (tstr->cap < tstr->len + rn); ctmp = realloc(tstr->str, tstr->cap); if (NULL == ctmp) { CERR("tstr_freadend realloc is error! path = %s!", path); fclose(txt); free(tstr->str); free(tstr); return NULL; } tstr->str = ctmp; } // 開始拷貝數據 memcpy(tstr->str + tstr->len, buf, rn); tstr->len += rn; } while (rn == BUFSIZ); fclose(txt); // 繼續構建數據, 最後一行補充一個\0 if (tstr->cap < tstr->len + 1) { do tstr->cap <<= 1; while (tstr->cap < tstr->len + 1); ctmp = realloc(tstr->str, tstr->cap); if (NULL == ctmp) { CERR("tstr_freadend realloc is end error! path = %s!", path); free(tstr->str); free(tstr); return NULL; } } tstr->str[tstr->len] = '\0'; tstr->len += 1; return tstr; }View Code
測試結果是通過的
到這裡基本上, 我們已經通過驗證,構建了最終的操作代碼, 歡迎嘗試用於性能提升上.
錯誤是難免的, 歡迎指正, 交流提高. O(∩_∩)O哈哈~
笑傲江湖曲 http://music.163.com/#/song?id=30031035