引言
最近几天在写普通平衡树这一题时,我没有使用我平常经常使用的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
<< " expecting: " << 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。