引言
最近幾天在寫普通平衡樹這一題時,我沒有使用我平常經常使用的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。
