C++中#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 
#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。

你可能感兴趣的:(语言)