#define宏定義的min與max函數的速度問題


引言


最近幾天在寫普通平衡樹這一題時,我沒有使用我平常經常使用的algorithm中的min與max函數(平常使用主要是因為懶得手打這樣使用比較標准),而是使用了#define宏定義的min與max函數,我認為這樣應該能加快一些速度,所以在我的代碼瘋狂TLE時我並沒有注意到這一點。在我接近debug到崩潰時,我把所有的預處理命令(本來這里想寫頭文件后來發現define的名字並不叫頭文件)都重打了一遍,再次提交時,發現竟然通過了這道題。我觀察了這些預處理命令,發現他們唯一的不同就是我把define宏定義函數改成了algorithm庫。在我的一臉蒙蔽之中,我測試了各種min,max函數的性能 。

這是我在這一題中會使用min/max函數的函數:

int lower(int now,int x) {
    if(!now) return -2147483646;
    if(bt[now].num<x) return max(bt[now].num,lower(bt[now].s[1],x));
    return lower(bt[now].s[0],x);
}

int upper(int now,int x) {
    if(!now) return 2147483647;
    if(bt[now].num>x) return min(bt[now].num,upper(bt[now].s[0],x));
    return upper(bt[now].s[1],x);
}

define宏定義的函數為:

#define max(a,b) ((a) > (b) ? (a) : (b))
#define min(a,b) ((a) < (b) ? (a) : (b))

測試


注:測試在洛谷在線IDE(C++無O2優化)上進行
我寫了下面幾行代碼來測試性能,min和max交替進行。

int main() {
    int n=1e7;
    int minx=n,maxx=0;
    for(int i=1;i<=n;++i) {
        minx=min(minx,n-i);
        maxx=max(maxx,i);
    }
    return 0;
}
algorithm庫 define宏定義函數 手敲函數(非內連,內部使用三目運算符)
60-80ms 20-30ms 60ms

結果顯示宏定義函數明顯比其他的要快,那為什么我的程序會因為宏定義函數TLE呢?

考慮到我寫的題中的min/max中有函數作為參數,所以我又寫了下面一個程序,來測試min/max中有函數時的性能。

int n=1e7;
int test(int i,int type) {
    return type==0?n-i:i;
}
int main() {
    int minx=n,maxx=0;
    for(int i=1;i<=n;++i) {
        minx=min(minx,test(i,0));
        maxx=max(maxx,test(i,1));
    }
    return 0;
}
algorithm庫 define宏定義函數 手敲函數
92ms 100ms 88ms

在我多次測試后,發現define宏定義函數總是最慢的。但是一次慢幾ms,對於n≤100000的普通平衡樹來說應該也不會讓本可以AC的代碼TLE。考慮到普通平衡樹一題中我在查詢前驅/后繼時的max/min中使用了遞歸函數,我再次寫了一段代碼進行測試。

int n=25;
int test(int i,int type,int I) {
    if(!i) return type==0?n-I:I;
    return max(type==0?n-i:i,test(i-1,type,I));
}
int main() {
    int minx=n,maxx=0;
    for(int i=1;i<=n;++i) {
        minx=min(minx,test(i,0,i));
        maxx=max(maxx,test(i,1,i));
    }
    return 0;
}

由於n=1e7時對於define運行時間過長,所以我改成了25(這差距好像有點大)。

algorithm庫 define宏定義函數 手敲函數
0ms 1020ms 0ms

這樣的情況下差距就十分明顯了,我也知道了為什么我的代碼會TLE,但是為什么會導致這樣呢?我找到了define的工作原理。

資料


我翻閱了 C++ Primer,3e ,在其中找到了答案。(C++ Primer,5e 好像已經把宏定義函數這一部分刪除了)

有時候強類型語言對於實現相對簡單的函數似乎是個障礙,例如雖下面
的函數 min()的算法很簡單,但是強類型語言要求我們為所有希望比較的
類型都實現一個實例

int min( int a, int b ) {
    return a < b ? a : b;
}
double min( double a, double b ) {
    return a < b ? a : b;
}

有一種方法可替代這種為每個 min()實例都顯式定義一個函數的方法,
這種方法很有吸引力,但是也很危險,那就是用預處理器的宏擴展設
施例如

 #define min(a,b) ((a) < (b) ? (a) : (b))

雖然該定義對於簡單的 min()調用都能正常工作,如

min(10,20);
min(10.0,20.0);

但是在復雜調用下它的行為是不可預期的,這是因為它的機制並不像函數
調用那樣工作,只是簡單地提供參數的替換,結果是它的兩個參數值都被
計算兩次,一次是在a和b的測試中,另一次是在宏的返回值被計算期間,
例如

#include <iostream>
#define min(a,b) ((a) < (b) ? (a) : (b))
const int size = 10;
int ia[size];
int main() {
    int elem_cnt = 0;
    int *p = &ia[0];
    // 計數數組元素的個數
    while ( min(p++,&ia[size]) != &ia[size] )
        ++elem_cnt;
    cout << "elem_cnt : " << elem_cnt
    << "\texpecting: " << size << endl;
    return 0;
}

這個程序給出了計算整型數組ia的元素個數的一種明顯繞彎的的方法。
min()的宏擴展在這種情況下會失敗,因為應用在指針實參p上的后置
遞增操作隨每次擴展而被應用了兩次,執行該程序的結果是下面不正
確的計算結果
elem_cnt:5 expecting:10

其中

它的兩個參數值都被計算兩次,一次是在a和b的測試中,另一次是在宏的返回值被計算期間。

解釋了原因。參數值會計算兩次,如果遞歸函數在min與max的define宏定義函數下調用了自己是非常可怕的,它會增加指數級別的時間復雜度。define宏定義因為不會真正調用函數的特性在一定情況下確實能增加速度,然而如果min與max的define宏定義函數的“實參”(其實它並不能叫做實參)中出現了一個復雜的計算的話,它會進行兩次計算,這大大拖慢了程序的速度。所以我建議如果在使用define宏定義函數時,如果傳值中出現了一個會進行時間較長的計算的函數的話,應該這樣使用:

int t=calc();  //假如calc()是一個需要經過大量計算的函數
ans=min(t,ans);

這樣會大大加快速度(或者除非卡常時否則干脆別用了)

經過測試,該代碼

#define min(a,b) ((a) < (b) ? (a) : (b))
#define max(a,b) ((a) > (b) ? (a) : (b))
int n=25;
int test(int i,int type,int I) {
    if(!i) return type==0?n-I:I;
    int t=test(i-1,type,I);  //防止重復計算
    return max(type==0?n-i:i,t);
}
int main() {
    int minx=n,maxx=0;
    for(int i=1;i<=n;++i) {
        minx=min(minx,test(i,0,i));
        maxx=max(maxx,test(i,1,i));
    }
    return 0;
}

速度已經下降到了0ms。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM