首先声明,本博文部份内容仅仅适用于ACM竞赛,并不适用于NOIP与OI竞赛,违规使用可能会遭竞赛处理,请慎重使用!遭遇任何状况都与本人无关哈=7=html
我也不想搞得那么严肃的,但真的有些函数在NOIP与OI竞赛中有相关规定不能使用,详细我也不知道各位要了解请自行去找比赛要求咯,固然在ACM竞赛中,没有限制函数,因此全部内容都适用于ACM竞赛。算法
那么什么是卡常数呢,简单来讲就是你和某神犇算法思路同样,结果他的AC了,你的TLE,复杂来讲就是程序被卡常数,通常指程序虽然渐进复杂度能够接受,可是因为实现/算法自己的时间常数因子较大,使得没法在OI/ACM等算法竞赛规定的时限内运行结束。数组
下面就是介绍各类各样的非(花)常(里)实(胡)用(哨)的优化方法的,若本文某些地方有错误或不明确的地方还请指出。=7=缓存
网上有不少说关于cin和scanf的介绍,以及关闭流输入等等优化方法,但这些都仍是有可能成为卡常数的地方,那么这个时候,咱们就能够本身写输出输出函数了。数据结构
下面一个简单的对读入数字的优化:并发
1 inline void read(int &sum) { 2 char ch = getchar(); 3 int tf = 0; 4 sum = 0; 5 while((ch < '0' || ch > '9') && (ch != '-')) ch = getchar(); 6 tf = ((ch == '-') && (ch = getchar())); 7 while(ch >= '0' && ch <= '9') sum = sum * 10+ (ch - 48), ch = getchar(); 8 (tf) && (sum =- sum); 9 }
由于getchar()是比scanf和cin快不少的,因此能够用这种方式优化不少,固然也能够写对其余各类类型输入的优化。app
而后就是进阶版优化,cstdio库里面有一个很是快并且和freopen和fopen完美兼容的函数就是fread,并且是整段读取,函数原型为:函数
1 size_t fread(void *buffer,size_t size,size_t count,FILE *stream);
做用:从stream中读取count个大小为size个字节的数据,放到数组buffer中,返回成功了多少个大小为为size个字节的数据。测试
因此咱们的代码能够更加优化为:优化
1 inline char nc() { 2 static char buf[1000000], *p1 = buf, *p2 = buf; 3 return p1 == p2 && (p2 = (p1 = buf) + fread (buf, 1, 1000000, stdin), p1 == p2) ? EOF : *p1++; 4 } 5 6 //#define nc getchar 7 inline void read(int &sum) { 8 char ch = nc(); 9 int tf = 0; 10 sum = 0; 11 while((ch < '0' || ch > '9') && (ch != '-')) ch = nc(); 12 tf = ((ch == '-') && (ch = nc())); 13 while(ch >= '0' && ch <= '9') sum = sum * 10+ (ch - 48), ch = nc(); 14 (tf) && (sum =- sum); 15 }
但要注意,因为这种方法是整段读取的,这也造就了它两个巨大的Bug:
scanf
,getchar
等其余读入方法混合使用。由于fread
是整段读取的,也就是说全部数据都被读取了,其余函数根本读取不到任何东西(只能从你的读取大小后面开始读),所以,全部类型的变量读入都必须本身写,上面的read
函数只支持int
类型。下面是测试,摘自LibreOJ,单位为毫秒
# | Language | [0,2) | [0,8) | [0,2^{15})) | [0,2^{31}) | [0,2^{63}) |
---|---|---|---|---|---|---|
fread | G++ 5.4.0 (-O2) | 13 | 13 | 39 | 70 | 111 |
getchar | G++ 5.4.0 (-O2) | 58 | 73 | 137 | 243 | 423 |
cin(关闭同步) | G++ 5.4.0 (-O2) | 161 | 147 | 205 | 270 | 394 |
cin | G++ 5.4.0 (-O2) | 442 | 429 | 706 | 1039 | 1683 |
scanf | G++ 5.4.0 (-O2) | 182 | 175 | 256 | 368 | 574 |
fread
以压倒性的优点碾压了其余全部方法,还能够注意到关流同步的cin比scanf快,关于为何不使用位运算的问题下面会说。
而后就是输出的优化,同理,putchar()会比printf快,因此,输出数字能够优化成:
1 // 优化前输出1-10000000:4.336秒 2 // 优化后输出1-10000000:1.897秒 3 void print( int k ){ 4 num = 0; 5 while( k > 0 ) ch[++num] = k % 10, k /= 10; 6 while( num ) 7 putchar( ch[num--]+48 ); 8 putchar( 32 ); 9 10 }
若是输出负数以及其余,就本身写一个或者百度啦,我这里就不贴了。其实大多数仍是对读入进行优化,输出通常用printf就能够了。
不少人都确定很喜欢用位运算吧,由于以为位运算是基于二进制操做,确定比普通加减乘除快不少,可是真的是全部的位运算操做都比常规操做快么。
1 x << 1; 2 x *= 2;
例如上面这两句,都是把x乘2,但真的用位运算会快么,其实他们理论上是同样的,在被g++翻译成汇编后,二者的语句都是
1 addl %eax, %eax1
它等价于 x = x + x。因此在这里位运算并无任何优化。那么把乘数扩大呢,好比乘10,x *= 10的汇编语言为
1 leal (%eax,%eax,4), %eax 2 addl %eax, %eax
翻译过来就是
1 x = x + x*4; 2 x = x + x;
而那些喜欢用(x << 3 + x << 1)的人本身斟酌!
可是位运算在某些地方是很是有用的,好比除法,右移的汇编代码为
1 movl _x, %eax 2 sarl %eax 3 movl %eax, _x 4 movl _x, %eax
而除二的汇编代码为
1 movl _x, %eax 2 movl %eax, %edx //(del) 3 shrl $31, %edx //(del) 4 addl %edx, %eax //(del) 5 sarl %eax 6 movl %eax, _x 7 movl _x, %eax
能够看到,右移会比除快不少。
这个其实可想而知&1快,仍是看下汇编代码吧,%2的汇编代码为
1 movl _x, %eax 2 movl $LC0, (%esp) 3 movl %eax, %edx //(del) 4 shrl $31, %edx //(del) 5 addl %edx, %eax //(del) 6 andl $1, %eax 7 subl %edx, %eax //(del) 8 movl %eax, 4(%esp) 9 movl %eax, _x
&1的汇编代码为
1 movl _x, %eax 2 movl $LC0, (%esp) 3 andl $1, %eax 4 movl %eax, 4(%esp) 5 movl %eax, _x
最开始学C语言两个变量交换都是先学三变量交换法,再学^这种操做,下面是(a ^= b ^= a ^= b)的汇编代码
1 movl _b, %edx 2 movl _a, %eax 3 xorl %edx, %eax 4 xorl %eax, %edx 5 xorl %edx, %eax 6 movl %eax, _a 7 xorl %eax, %eax 8 movl %edx, _b
再来看看(int t = a;a = b,b = t;)的汇编代码
1 movl _a, %eax 2 movl _b, %edx 3 movl %eax, _b 4 xorl %eax, %eax 5 movl %edx, _a
谁慢谁快一眼就知道了,之后swap再无Xor。
网上有不少奇奇怪怪的位运算技巧,但有一些真的使人很无语,没有优化不说,大大下降了代码可读性,在我看来,都是些花里胡哨的操做,好比取绝对值(n ^ (n >> 31)) - (n >> 31),取两个数的最大值b & ((a - b) >> 31) | a & ( ~(a - b) >> 31),取两个数的最小值a & ((a - b) >> 31) | b & ( ~(a-b) >> 31 )。恕我愚钝,这些代码一眼看上去根本不知道在干吗,还有那个取绝对值的和abs(x),谁快都不用说了。
可是位运算仍是有不少好(骚)操做的,例如:
lowbit函数 : x & (-x)
判断是否是2的幂:x > 0 ? ( x & (x - 1)) == 0 : false
emmm……还有不少,我就不介绍了(我就知道这两个=7=)
acm不可避免会有条件语句,if-else也好,?:也好,switch也好,那么问题来了,最后用哪一种呢,让咱们一一道来。
网上不少说if比?:慢,可是其实不是这样的,两者的汇编除了文件名不同其余都如出一辙。其实不是?:比if快而是?:比if-else快。
有什么区别吗?你须要先弄清楚if-else的工做原理。
if就像一个铁路分叉道口,在CPU底层这种通信及其很差的地方,在火车开近以前,鬼知道火车要往哪边开,那怎么办?猜!
若是猜对了,它直接经过,继续前行。
若是猜错了,车头将中止,倒回去,你将铁轨扳至反方向,火车从新启动,驶过道口。
若是是第一种状况,那很好办,那第二种呢?时间就这么浪过去了,假如你很是不走运,那你的程序就会卡在中止-回滚-热启动的过程当中。
上面猜的过程就是分支预测。
虽然是猜,但编译器也不是随便乱猜,那怎么猜呢?答案是分析以前的运行记录。假设以前不少次都是true,那此次就猜true,若是最近连续不少次都是false,那此次就猜false。
但这一切都要看你的CPU了,所以,通常把容易成立的条件写在前面判断,把不容易成立的条件放在else那里。
可是?:消除了分支预测,所以在布尔表达式的结果近似随机的时候?:更快,不然就是if更快啦。
gcc存在内置函数:__builtin_expect(!!(x), tf),他不会改变x的值,仅仅只是减小跳转次数,当tf为true时表示x很是可能为true,反之同理。
用法就是if(__builtin_expect(!!(x),0)) 或者把0换为1,这样在if猜的时候就会优先猜x为true或是false,达到优化效果。
这个东西仍是有必要提一下,当switch没有default的时候,switch会比if-else快,由于他是直接跳转而不是逐条判断,但加了default以后,switch也就变成了无脑判断模式,至于为何会这样,各位就自行研究咯=7=
咱们知道&&和||是两个短路运算符,什么叫短路运算符,就是一旦能够肯定了表达式的真假值时候,就直接返回真假值了,好比下面代码
1 int n = 0; 2 3 n && ++n; 4 5 //这里n的值仍是0 6 7 !n || ++n; 8 9 //这里n的值仍是0
可是上面的两句代码等同于什么呢?等于
int n = 0; if(n){ ++n; } if(!(!n)){ ++n; }
利用这个特点(你才特点),咱们有些时候就能够不须要在作if的无脑判断了,也就是
但这些并非短路运算符的精髓,短路运算符的精髓不只在于优化时间,更是能够防止程序出错。
1 double t = rand(); 2 if (t / RAND_MAX < 0.2 && t != 0) 3 printf ("%d", t); 4 5 double t = rand(); 6 if (t != 0 && t / RAND_MAX < 0.2) 7 printf ("%d", t);
这两种判断,谁快谁慢。但对于CPU来讲颇有区别。第一段代码中的t/RAND_MAX<0.2为true的几率约为 20%,但t!=0为true的几率约为1/RAND_MAX,明显小于20%
所以,若是把计算一个不含逻辑运算符布尔表达式的计算次数设为 1 次,设计算了 X 次,则对于第 1 段代码,X 的数学指望为 6/5 次,但对于第二段代码,X 的数学指望2*(RAND_MAX-1) / RAND_MAX为 ,远远大于第一段代码。
不只不一样位置会优化时间,更是会防止程序错误,例如kuangbin搜索专题有题是Catch the Cow,就是搜索,不过判断走没走过得判断vis[n]和n < 1e6,我最最开始写的vis[n] && n < 1e6,提交上去RE了,看了好久才发现是这里的缘由,得先判断n < 1e6,再作下一步操做。
因此, 遇到A&&B时,优先把可能为false的表达式放在前面。遇到A||B时,优先把可能为true的表达式放在前面。但也不必定是绝对这样,还得结合题目。
不少人喜欢用if(x == true)这种形式,但其实if(x)就好了,在可读性等方面都没有变化。并且不要开bool数组,int是最快的(缘由暂时不知道)。
逗号运算符若干条语句能够经过逗号运算符合并成一条语句。 例如t=a;a=b;b=t;能够写成t=a,a=b,b=t;有什么用吗?它的返回值。
int x=(1,2,3,4,5);
猜一猜,上面的语句执行完后x的值是多少? 答案是 5 没错,逗号运算符的返回值就是最后一个的值。并且逗号表达式比分号快不少不少,真的。
inline
:由编译器在编译时会在主程序中把函数的内容直接展开替换,减小了内存访问,可是这并非适用于各类复杂以及递归式的函数,复杂函数编译器会自动忽略inline
1 int max(int a, int b){return a>b?a:b;}//原函数 2 inline int max(int a, int b){return a>b?a:b;}//直接加inline就行了。
register
:对于一些频繁使用的变量,能够声明时加上该关键字,运行时可能会把该变量放到CPU寄存器中,只是可能,由于寄存器的空间是有限的,不保证有效。特别是你变量多的时候,通常仍是丢到内存里面的。
比较下面两段程序:
1 register int a=0; 2 for(register int i=1;i<=999999999;i++)a++; 3 4 int a=0; 5 for(int i=1;i<=999999999;i++)a++;
优化:0.2826 second
不优化:1.944 second
1 //设模数为 mod 2 inline int inc(int x,int v,int mod){x+=v;return x>=mod?x-mod:x;}//代替取模+ 3 inline int dec(int x,int v,int mod){x-=v;return x<0?x+mod:x;}//代替取模-
用++i代替i++,后置++须要保存临时变量以返回以前的值,在 STL 中很是慢。
若是要常常调用a[x],b[x],c[x]这样的数组,把它们写在同一个结构体里面会变快一些,好比f[x].a, f[x].b, f[x].c
指针比下标快,数组在用方括号时作了一次加法才能取地址!因此在那些计算量超大的数据结构中,你每次都多作了一次加法!!!在 64 位系统下是 long long 相加,效率可想而知。
STL快可是也包含了不少可能你用不到的东西,因此最快的就是你本身手写STL=7=,反正我写不来。
1 void Init(int *d, int n){ 2 for(int i = 0; i < n; i++) 3 d[i] = 0; 4 } 5 6 7 void Init(int *d, int n){ 8 int il 9 for(int i = 0; i < n; i+= 4){ //每次迭代处理4个元素 10 d[i] = 0; 11 d[i + 1] = 0; 12 d[i + 2] = 0; 13 d[i + 3] = 0; 14 } 15 for(; i < n; i++)//将剩余未处理的元素再依次初始化 16 d[i] = 0; 17 }
都是同一个操做,但大家以为谁快呢,用下面的比第一段代码快了不止一倍,循环展开也许只是表面,在缓存和寄存器容许的状况下一条语句内大量的展开运算会刺激 CPU 并发
好像没什么要讲的了呢,网上还有一些很邪门的优化方式,我以为就不必了,能大体知道一些优化流程就好了,好比读入还有mmap但用这个不是很了解的话可能还会用出事,因此别不必那么追求极限了。本身以为讲的仍是挺多挺全面的,如果哪里有错误或者没讲到的地方还请指出。