自定函式效能測試為常用之一環,吾人日前同一個函式 (log2) 進行了大量之測試 (有15個副函式要測試),由於之前都「亂寫」,故在維護、新增、移除掉副函式時都不方便,於此提供一些基本之技巧與心得。

1. 測試前註明環境規格

這點很重要!加註的東西包含 CPU 規格、RAM 大小、顯卡型號(這個最好加)、Compiler 版臺、Debug/Release mode、是否有開優化 等,上面這些原因都將導致測試結果不一。特別是 Debug / Release / 優化要註明 ,因 Debug / Release 差很多!若不想測試那麼多,那請調成 Release Mode (此為吾人常犯之誤!)

VC 2008 調 release / debug mode :專案 -> 屬性 -> 組態管理員 -> 切換 Release/Debug
VC 2008 調最佳化:專案 -> 屬性 -> 組態屬性 -> C/C++
VC 2008 生成 asm:專案 -> 屬性 -> 組態屬性 -> C/C++ -> 命令列 ,右邊其他選項輸入 /Fas

2. 進行長期測時

如何測試效能一直都是個讓人爭議的議題,有人從看編出來的 asm code 去看效能好不好,平台產生 asm code 之方式如下

DEV-C++ : CC1.exe test1.c
Visual C++:CL.exe test1.c /FAs    (或 /FA, /FAc , /FAu,詳細內容請下 CL.exe /?)

但即使知道 asm code,硬體對於其效能之實作也不盡相同,如哪型號 CPU 在 FPU 之處理較好、哪型號 CPU 在整數處理較好,若再考慮 cache miss 等問題,吾人也較不偏向此測試用法 ( 其實是不太會分析 ),故進行的是時間測時分析,關於測時有哪些方式,可參考 計時器整理 這篇。

3. 時間測時分析

這裡考慮下面四個部份

(3.1) 多次迭代
(3.2) 多次計時
(3.3) 每次迭代完都輸出
(3.4) 輸出平均計時結果

雖計時器整理該篇已提供了五種測時方式 (有空再把第六種 __asm 補上),最精準的為 QueryPerformance 類之 API,但吾人認為別太依賴它。一方面多測幾次結果可能會漂,另一方面也有部份文章指出,QueryPerformance 穩定性並不高 (準歸準,但不代表穩定性好),於是較建議直接調用 clock() 方式進行測時,最大的原因也是因為它為標準之函式庫。

但 clock() 之誤差仍有數十個毫秒之誤差,故較建議大批量之測試,不要只測一、二次 (小函式測 100 次都嫌太少),故比較建議進行大回圈之測式,也可避免測時誤差方面之問題;另也可多測幾次把結果平均下來,這部份全視測試之規劃。架構碼大致如下

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define TIMES 5
#define ITERA 10000000

viod test_func() { //do something }
int main()
{
    unsigned itera, times;
    clock_t t1,  td, ts;

    for(times=0, ts=0; times!=TIMES; ++times){
        t1 = clock();
        for(itera=0; itera!=ITERA; ++itera){
            test_func();
        }
        td = clock() - t1;
        ts += td; // 計算總測時

        // 輸出詳細資料
        printf("%u: %ld\n", times, td);
    }
    printf("ave : %lf\n", (double)td/TIMES);
    return 0;
}

 

大致上架構是長這樣,上面輸出是幾個 clock,而不是秒數,若要輸出秒數的話再除上 CLK_TCK 便可。但這並非為好的架構,下述繼續討探。

4. 注意優化後帶來之副作用

以 VC 而言,/O2 (速度最佳化) 可能會帶來以下之副作用,考慮以下副函式

int IsEven(int x){ return x & 1; }

 

(4.1) 空回圈忽略:若是寫成 for(i=0; i!=N; ++i); ,這為空回圈,這段很可能會不被執行。
(4.2) 副函式不予以賦值:大多自定義之副函式都會有傳回值,常有人會計較,原因是 assign to varialbe 也需要一點點時間,於是程式碼中可能就這麼寫

for(i=0; i!=N; ++i) IsEven(i);

 

很遺憾的,這段碼在 VC 在,由於 IsEven 最後沒有變數去接傳回值,故 IsEven 會被拿掉,拿掉後就變成了 for(i=0; i!=N; ++i); 空回圈,鑑於 4.1 之法則,這段碼將會整段被拿掉。

(4.3) 副函式賦值變數卻不再調用該變數:鑑於上述之情形,故有了一點點想法,那就把傳回值 assign to variable,於是有了下面這段 code

for(i=0, rst=0; i!=N; ++i) rst = IsEven(i);

 

很遺憾的,這段 code 還是不能過!原因是 rst 接完之後,程式以下就再也沒有要做輸入、輸出、運算的動作,於是 compiler 也會把這段回圈拿掉。

建議的寫法如下

for(i=0, rst=0; i!=N; ++i) rst += IsEven(i);
printf("%d\n", rst);

 

這樣的寫法除了能解決 compiler 優化而不執行該函式之外,同時最後把 rst 輸出,去比對撰寫之副函式彼此間之結果是否相同,作為簡單之邏輯判斷。

4. 加入 macro 之條件式輸出

在上一段程式碼,我們每次都會輸出詳細資料,但事實上並不是每次都要拿該訊息,有時只是想看平均時間,如果拿掉的話每次都要回去改,這只有一段測試而已,如果有五個函式要測試就要改五段,實為不智之舉,於是使用下面這段 maco 進行條件式之輸出

#define DETAIL 1

int main()
{
    for(times=0, ts=0; times!=TIMES; ++times){
        t1 = clock();
        for(itera=0; itera!=ITERA; ++itera){
            test_func();
        }
        td = clock() - t1;
        ts += td; // 計算總測時

        // 輸出詳細資料
        #if DETAIL
            printf("%u: %ld\n", times, td);
       #endif
    }
}

 

若不想輸出詳細之資料,到時將 DETAIL 改回 0 便可。

5. 擅用 IO Redirect

C/C++ 裡面最簡單做 redirect 方式便為 freopen。有時在測試完後,是要將結果紀錄到文字檔裡面去,但每次都要改程式碼便相當麻煩,於是比較建議用 io redirect 方式去做指定測試,程式碼如下所示

#include <stdio.h>
#include <stdlib.h>
#define REDIRECT 1
#define FILENAME "rst.txt"

int main()
{
    #if REDIRECT
        freopen(FILENAME, "w", stdout);
    #end
    return 0;
}

 

若要寫到 rst.txt 檔案裡,將 REDIRECT 設為 1;若要螢幕輸出 (應說是標準輸出裝置),則將 REDIRECT 設為 0。同時在輸出格式也最好以 rst.txt 之格式為主,意指可考慮輸出格式為 excel 所開啟,方便觀察與整理結果。

6. 擅用指標陣列

這點很少人會這麼做,吾人一開始認為這也很麻煩,但後來發現這才是正確的做法!不論自己寫了幾個副函式,請用指標陣列去將它包起來!因倒時要再擴充一些其它方法時也比較方便。

7. 範例

這裡給的範例是 log2 之範例,鑑於以上原則程式碼大略如下所示。

// ====================================
#include <time.h>
#include <stdlib.h>
#include <math.h>
#include <stdio.h>

// ====================================
#define TIMES 10        /* 測試次數 */
#define ITERA 1000000    /* 迭代次數 */

#define IOREDIRECT 0
#define DETAIL     0

/*定義函式指標, plog2 為指向 unsigned func(unsigned) 之指標*/
typedef unsigned (*plog2) (unsigned);

// ====================================
// function declare
unsigned log2_shift(unsigned x);
unsigned log2_logx(unsigned x);


int main()
{
    unsigned f,times, itera;
    unsigned rst;
    clock_t t1, td, ts;
    plog2 pfunc[] = {log2_shift, log2_logx};
    char *func_name[] = {"log2_shift", "log2_logx"};
    unsigned func_cnt = sizeof(pfunc) / sizeof(pfunc[0]);

#if IOREDIRECT
    freopen("rst.txt", "w", stdout);
#endif

    for(f=0; f!=func_cnt; ++f){
       for(times=0, ts=0; times!=TIMES; ++times){
          rst=0;
          t1 = clock();
          for(itera=0; itera!=ITERA; ++itera){
             rst+=pfunc[f](itera);
          }
          td = clock()-t1;
          ts+=td;
#if DETAIL
          printf("%s\t%2u\t%ld\t%u\n",func_name[f], times, td,rst);
#endif
       }
       printf("%s\tave\t%lf\n", func_name[f], (double)ts/TIMES);
    }
    return 0;
}

/////////////////////////////////////////////////
/* function body */

// ====================================
// log2_shift
unsigned log2_shift(unsigned x)
{
    unsigned i=0;
    while (x>>=1) ++i;
    return i;
}

// ====================================
// log2_logx
unsigned log2_logx(unsigned x)
{
    static const double LOG_INVERSE = 1.442695041;
    return (unsigned)(log(x) * LOG_INVERSE);
}

 

8. 缺點改善 (感謝 Jacob 指導)

(8.1) loop 寫法

for(i=0; i!=N; ++i) 改為 do{ /* do something */} while(--i); 避免 inc 之影響。

(8.2) 平均值誤差大

clock() 函式本身誤差便大,若是一直用 td += ts,累計下來誤差更大,故別做每次累計之動作。

(8.3) typedef 字首做適當大寫

上文之 typedef unsigned (*plog2) (unsigned);

使用 typedef unsigned (*pLog2) (unsigned);

較佳。

(8.4) 測試次數可考慮只改一次

鑑於計時準度,可把 #define TIMES 及相關 loop 整個拿掉。


 

code 看起來相當的醜,若有更多之技巧,歡迎不吝指教。

 

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