2013年10月14日 星期一

講講 CFile、fstream 與 傳統 C 程式庫對於檔案讀取速度上的比較

在不發文的話,就要被樓主 K 頭了,所以今天就來講一個很實用的主題,就是在 C/C++ 中
developer(稱 developer,聽起來比較高級一點,programmer 讓人有一種做苦工的感覺)
常常需要打開硬碟內的檔案讀取資料吧,也許是一些設備存檔出來的報表資料,也許是一些
實驗數據,或者是一些軟體播放影音檔,不管用了多麼深奧的解碼技術,這些軟體的原始碼
一定都會有對檔案進行讀寫的動作,但是存取檔案的技巧其實有很多種類,這也是常常讓人
迷惑的地方,所以吾人就來講講一種我們常常會用到的檔案操作物件,來解解大家的迷惑
並且比較一下這些物件讀取檔案的速度。
其實讀檔常用的物件主要有三種:

首先常常用 MFC 的 developer 這些應該都有一種堅持 MFC 風格的偏執,就是要 MFC 就
整個程式都用 MFC,這類 developer 通常很不喜歡非 MFC 的外來物,他們通常會在 MFC
裡面先找 MFC 有沒有他們要的功能,沒有的話才會引入外部的程式庫,例如  OpenGL
即便引入外部程式庫,這些對 MFC 有狂熱堅持的 developer 也一定會封裝成 MFC 的類別
風格好跟 MFC 搭配,講了這麼多,那 MFC 有沒有已經提供存取檔案的類別呢?

這個類別就是 MFC 提供的 CFile,它的功能實在多到講不完
它是一個繼承 MFC 基本物件 CObject 的操作檔案的工具類別,吾人本身因為也是長年習慣於
開發傳統 Win32 程式與 MFC 程式,自然也是 CFile 的愛用者,CFile 真的很好用,有時候偷懶
寫傳統 Win32 程式要處理檔案還是直接叫用 MFC 內的 CFile 來解決,因為它封裝了很多應用
程式介面的細節,省去記憶 API 的麻煩,當然本文只會提最簡單的開檔讀檔的動作,不會牽
涉一些與 MFC 特有的整合動作。

第二個競爭者是 C++ 標準程式庫 Standard Template Library 內建的 fstream, 老實說,吾人
自己到現在還是不太喜歡 Standard Template Library 內那種像天書的語法還有看起來像蝌蚪文
的那些參數名稱,不知道我在說甚麼的人去開 STL 的一些標頭檔看看就懂了,反正你還是
會回頭去看書找範例用,或是到 MSDN 上找 Example Code 來貼,像敝人從來就記不起來
ios:: 裡面有啥可以用(囧),不過因為 fstream 很常用,所以它是第二個參賽者,像網路上
有一篇很有名在教學 Quake 的 MD2 3D模型檔案的內容,作者附的範例裡面有一個輔助程式
庫專門讀取 TGA 圖檔,那個類別檔案操作的部分就採用了 fstream。

第三個競爭者是我們永遠的好朋友 C 標準程式庫內的 FILE 介面函式,相信它經常
陪伴了讀者們經歷過大學機算機課程甚至研究所乃至於工作,所以它當然是我們永遠的好朋
友,簡單易用,歷史悠久,尤其讀者假如又是 Linux 程式設計的愛好者,那更會使用標準程式
庫來工作,而不會故意去用系統專屬的一些古怪 API,總之,C 語言作為除了組合語言之外
的原生程式語言,FILE 介面的檔案操作函式也是常常被使用,而且它們的移植性很強,例如
你要寫一個讀取 TGA 圖檔的函式庫要有可移植性,檔案操作的部分用 FILE 介面是一個很好
的選擇,將來要移植到別的平台也會比較好修改程式碼。

所以一個有趣的問題就出現囉,誰讀取檔案的速度比較快呢?
就是因為這個問題我也從來沒想過,所以才特別寫了一支程式試試看
先把原始碼 post 給大家~~~~

/*
==============================================================
ReadFileBench.cpp

針對相同檔案使用不同檔案存取介面測試讀取速度
==============================================================
*/

/*
==============================================================
下列巨集會定義最低平台需求。最低平台需求是指各種版本的
 Windows、Internet Explorer 中,具備執行應用程式所需功能的
 最早版本。巨集的作用,是在指定或更新版本的平台上啟用
 所有可用的功能。

 如果您有必須優先選取的平台,請修改下列定義。
 參考 MSDN 取得不同平台對應值的最新資訊。
==============================================================
*/
#ifndef _WIN32_WINNT        // 指定最低平台需求為 Windows Vista。
#define _WIN32_WINNT 0x0600 // 將它變更為針對 Windows 其他版本的適當值。
#endif

#include <afxwin.h>
#include <stdio.h>
#include <tchar.h>
#include <string>
#include <iostream>
#include <fstream>
using namespace std;

const TCHAR g_szFileName[] = _T("c:\\flip.exe");

unsigned int test1(int ntries) // MFC: CFile object
{
    BYTE   *iobuf = NULL;
    size_t sz = 0;
    CFile  f;

    UINT start = GetTickCount();
    
    for(int i = 0; i < ntries; i++)
    {
        if(f.Open(g_szFileName, CFile::modeRead | CFile::shareDenyWrite))
        {
            if(iobuf == NULL)
            {
                sz = (size_t)f.GetLength();
                iobuf = new BYTE[sz];
            }
            f.Read(iobuf, sz);
            f.Close();
        }
    }

    if(iobuf != NULL) delete[] iobuf;

    return GetTickCount() - start;
}

unsigned int test2(int ntries) // C++ STL library: fstream
{
    BYTE    *iobuf = NULL;
    size_t  sz = 0;
    fstream f;

    UINT start = GetTickCount();
    
    for(int i = 0; i < ntries; i++)
    {
        f.open(g_szFileName, ios::in | ios::binary);
        if(f.is_open())
        {
            if(iobuf == NULL)
            {
                f.seekg(0, ios::end);
                sz = f.tellp();
                f.seekg(0, ios::beg);
                iobuf = new BYTE[sz];
            }
            f.read((char*)iobuf, sz);
            f.close();
        }
    }
    
    if(iobuf != NULL) delete[] iobuf;

    return GetTickCount() - start;
}

unsigned int test3(int ntries) // Standard C file library 
{
    FILE *fp;
    BYTE *iobuf = NULL;
    UINT sz;

    UINT start = GetTickCount();
    
    for(int i = 0; i < ntries; i++)
    {
        _tfopen_s(&fp, g_szFileName, _T("rb"));
        if(fp)
        {
            if(iobuf == NULL)
            {
                fseek(fp, 0, SEEK_END); // 檔案指標從末端偏移,偏移量為零
                sz = ftell(fp);         // 用 ftell 回傳檔案指標位置,即是檔案大小
                fseek(fp, 0, SEEK_SET); // 檔案指標從頭端偏移,偏移量為零
                iobuf = new BYTE[sz];
            }
            LONG x = fread(iobuf, 1, sz, fp);
            fclose(fp);
        }
    }

    if(iobuf != NULL) delete[] iobuf;
    
    return GetTickCount() - start;
}

int _tmain(int argc, _TCHAR* argv[])
{
    int  nRetCode = 0;

    // Initialize MFC and print and error on failure
    if (!AfxWinInit(::GetModuleHandle(NULL), NULL, ::GetCommandLine(), 0))
    {
        // TODO: change error code to suit your needs
        cerr << _T("Fatal Error: MFC initialization failed") << endl;
        nRetCode = 1;
    }
    else
    {
        // TODO: code your application's behavior here.
        cout << "CFile test: "   << test1(1000) << " ms" << endl;
        cout << "fstream test: " << test2(1000) << " ms" << endl;
        cout << "FILE test: "    << test3(1000) << " ms" << endl;
        cin.ignore();
    }

    return nRetCode;
}

各位讀者可以看到,CFile 物件封裝的很簡單,developer 可以很輕易的用 GetLength 獲得檔案
的長度,從而知道要配置多少記憶體,C++ 跟 C 標準程式庫就比較麻煩一點,用 fseek 先一
到檔案末尾取得指標位址後在移動回檔案指標頭部,各位讀者可以看到 fstream 的表達雖然
用物件的方式來做,其實還是可以依稀看到類似 FILE 介面函式的那種操作,因為底層最後
還是會呼叫 FILE 函式介面。

下一步要講解的就是如何在非 MFC 的程式內呼叫 MFC 內的工具類別,讀者首先至少要會
MFC 的初始化函式 AfxWinInit,第一項參數要目前程式的 hInstance,用 GetModuleHandle
就可以獲得目前程式的 hInstance,第二項參數 hPrevInstance,早就不用了給它 NULL 就好了
第三項參數要系統命令列字串指標,用 GetCommandLine 丟給它,最後一項參數給 0,反正
又沒有要寫視窗,全部參數跟 WinMain 長的一模一樣,這些就是要傳給隱藏在 MFC 類別庫
內的 WinMain 程序啦,所以有寫過 MFC 程式的讀者會知道,類別式的 GUI 程式是找不到進入
點的啦,要懂的找訊息映射表看看甚麼 UI 的 ID 連接甚麼函式,別傻傻的一行一行看。

還有記得要開啟 MFC 的使用跟引入 afxwin.h



還有這次沒用另外一個老朋友 printf ,改用了 C++ Console 程式常用的 cout,自己複習一下
也讓讀者們複習一下,當然想改用 Console API 也可以,不過以前文章為了教學寫過了,現在
我們的目的並不是這個,能簡單顯示資訊就好。

我們來跑跑看程式會出甚麼結果(如下圖):



從圖上的結果可以知道,很明顯奪冠者是 FILE 介面函式,在四次的測試中,每一次都遙遙
領先,這證明了原始 C 函式的效率的確很好,沒甚麼好評論,但是 CFile 跟 fstream 就很詭異
每次測試都不一樣,沒有絕對的領先,讀者可以自己開個專案在自己電腦上試試看,可能的
說明應該講兩者效率差不多,畢竟都是微軟自己實作的版本,這樣的結果可能跟系統本身
對檔案的操作有一些快取機制存在有關,一個很明顯的例子就是當我用 USB 封包截取程式
對 USB Mass Storage 作檔案複製或讀取有時候會發現 USB 封包截取程式一點反應也沒有,有
時候就會出現一堆操作 USB 的 IRP 請求,以上這項測試是放在硬碟 C 槽中。

下一項測試更詭異了,當我把檔案放在 W 槽這個 RAMDISK中各位可以看看下面測試圖


囧,放在 RAMDISK 測試結果真是出乎意料之外,似乎沒啥優勢,顯然作業系統有一些神秘
的機制從中作梗,比放在實體硬體還慢,但是有一點值得注意,讀取速度似乎都很平均,而
且大多坐落在 250 ms 上下,而且 FILE 介面函式非常穩定,連續測試四次區然都很穩定的維持
在 250 ms,而 C++ 構築的 fstream 與 CFile 顯然內部程式有較多的虛耗較慢,但是一樣有時
CFile 快 有時 fstream 快 甚至還有相等的情況出現,但整體來說,檔案放在 RAMDISK 讀取
不管選用哪一種方法,整個讀取速度比較均等。

我測試的檔案大小是 55KBytes,程式會開檔整個讀取並且寫入 Buffer 然後關檔每種方法連續
測試一千次來計算花費時間,讀者也許可以在自己電腦上試試看其他更大的檔案會有甚麼
其它的結果,敝人就沒有測試了。

第四種應該要有 Windows API 原生的檔案操作 API 讀檔測試,這部分我沒有寫,讀者可以
參考 MSDN 用 CreateFile ReadFile GetFileSize CloseHandle 就可以達成。

其實檔案操作並不是只有這幾種方法,還有一種被檔案映射到行程空間操作,使得操作檔案
就跟操作記憶體的方法一樣,讓一些需要記憶體位址的操作函式也可以直接操作檔案,這種
技術就稱為記憶體映射檔,本文就不討論了,讓讀者知道一下即可,操作複雜的檔案格式時
這種技術就很管用,像解析 PE 格式的程式或是操作影音檔案就會用到,這其實就是一種
MMIO 的技術,這種技術最常被用在寫驅動程式來操作硬體設備上的 Register,因為一般來說
2GB 的 Ring 3 虛擬記憶體空間一般都用不滿,系統就有一些機置乾脆把這些空間映射到 IO 空
間來操作 IO,同樣的情況在 Ring 0 也是一樣。

另外由於 C++ 標準程式庫是強型態檢查,所以 fstream 的 read 第一項參數要吃 char* 你得要
乖乖轉換一下,否則編譯會出現錯誤。

沒有留言:

張貼留言