前言
卡常数操作并不是想象的那么简单 ComeIntoPower dalao 的建议让我知道了这里的深度 (实在做不来还是学算法吧 QAQ~~~
适当的优化可以帮你只能想出暴力的算法多 A 几个点,但是最终嘛
算法的学习和研究最有效 QwQ
内容
1. 读入优化
2. 宏定义
3. 选用的数据类型
4. 语句与运算优化
5. 手写数据结构
6. 对拍
One . 读入优化
这个基本都知道吧,我也就不细讲了
读入基于 fread
,这里实现的只能读入字符和整形,至于输出+stack
,基于 putchar
就好了,反正输出量又不是很大 (只是我懒,不愿写
我先说一下关于位运算什么想多了,你把 s*10
写成 位运算 只会让你的程序更慢,因为同样在 g++ 把程序编译之后的代码,后者比前者还多了 1 条语句 。
并且 乘法的位运算和直接 *
速度是一样的 (在汇编里均为等价….),而且 如果
当然位运算在除法里还是很有用的,对 2 的幂的运算,以及判断奇偶的时候
Second . 宏定义
宏定义是个啥? ——————— 也就是 define
#define
是个好东西啊 OUO, 不仅可以替换变量名使你的代码更短,还可以定义函数 (实则就相当于直接替换),还有奇特的用法,那么
不过请注意 define
的本质是替换所以 如果宏的参数出现 ++x
或者 x++
之类的 请另外加语句除去,否则 x
在宏里会重复计算,导致你调试到死也不知道为什么答案不对
Third . 数据类型
1. 首先速度的话 int
好像是最快的,所以不考虑内存,精度的话尽量都用 int 存吧
2.vector
里 iterator
比使用下标 访问 快一点了(虽然 STL 普遍比较慢,如果开 O2,vector 取下标和数组取下标速度差不多(不过 vector 要判越界,当然不开 O2 你最好连 vector 都不要用
3. 数据极为强大的最大流使用 vector
存图,不信的话,你可以自己试试 原因在于数据太过巨大,vector
动态的劣势已降至极低,而大量访问连续的内存地址显然比前向星更优
4. 提高时空局部性
提高时间与空间局部性即可使你的程序对高速缓存友好。
如果你写一个树剖,上来先开一大堆数组:
int siz[maxn], top[maxn], son[maxn], fa[maxn], depth[maxn];
而访问的时候,却总是siz[i], top[i]
之类的连着访问,这样做空间局部性极差,所以为了提高运行速度,我们可以把这些东西扔进结构体里,由于结构体内的元素在内存里是按顺序排列的,所以可以提高空间局部性, 虽然有时也可能变慢 QAQ
避免使用步长为 2 的 n 次幂的内存引用由于高速缓存不是全相连的,使用步长为 2 的 n 次幂的内存引用模式所以会导致每次访问都不命中,效率会比较低不过,基本很少会开 2^n 的空间吧 2333。
5.《Bitset》简介
Four. 语句与运算优化
前言:一般我们的程序会编译器转化为汇编语言,
而一些语句在汇编里是等价的,所以不要轻易相信网上的一些什么语句比什么语句快之类的, 如果有对语句相同觉得疑惑的话请自行去这里验证 c++ -> 汇编
编译器远比你想象的聪明,所以最适合优化的便是所谓 “少一些变量”,“少一些判断” 等人工的的优化方式了
位运算 技巧
II. 条件语句
当你的 if(A && B
)中 如果 B
为 false
的可能性比 A
大的话,你就需要互换位置,因为当第一个表达式为 false
的时候,程序就不会执行后面的表达式
III. 关于取模的优化 OvO。
因为避免了模数的除指令运算很费事,所以我们使用加避免了除指令。
IV. 对于矩阵乘法可以通过调整 for
的顺序使得访问相对连续使得程序运行加快
也就是说减少不必要的计算,对于一个重复计算的值存入临时变量, 并且把访问量越大的尽量放在前面枚举。这样指针的跃动距离会少一些。
V. 顺序访问
顺序访问的话缓存可以实现高速遍历(相对于随机顺序)。但是顺序访问与逆序访问,速度是差不多的,不会对缓存造成什么影响,所以我们在遍历数组的时候要尽量修改跳动的指针为连续的指针
VI. 循环展开+多路并行
在不超过 cache 大小的情况下循环展开越深优化越大
由于 cpu 整数加法运算器有多个,而同一个时间可以最多运算两个整数加法,通过这种方法可以增加流水线吞吐量。实际上,编译器会在第二次隐式汇编优化时候做这个优化,如果你把 gcc 开到 o3 以上的优化程度就可以自动在汇编指令层级变成这种形式
不过当展开次数过多时,性能反而下降,因为寄存器不够用 也就是 寄存器溢出,比较玄学 , 所以你可以使用 Dev c++的调试看看寄存器是否溢出,或者用 clock() 函数看看怎么展开耗时少。
循环次数未知的循环并行性很差,基本没有研究的价值。如果循环展开 k
次,就可以把上限设为 n-k+1
,那么最大循环索引 i+k-1
将会比 n
小。然后,再加上第二个循环,以每次处理一个元素的方式处理数组的最后几个元素
优点
1. 分支预测失败减少。
2. 如果循环体内语句没有数据相关,增加了并发执行的机会。
3. 可以在执行时动态循环展开。这种情况在编译时也不可能掌握。
缺点
1. 代码膨胀
2. 代码可读性降低,除非是编译器透明执行循环展开。
3. 循环体内包含递归可能会降低循环展开的得益。
摘自维基百科
VIII. CPU 并发
注意:在使用这个技巧时,需要自行判断能否使用,否则后果…
这个技巧看似简单,但能带来常数级别的飞越,可能出现的情况 n2过百万,暴力踩标程。QAQ , 差不多就这么个鬼玩意
使用条件:
循环展开后,可以方便地用大量简单的运算完成对答案的更新。
观察到你的寄存器并不会溢出。
例题 计算 ∑i=from 1 to 30000 ∑j=from 1 to 30000(A[i] / B[j]) 其中 $B[j]<=64,A[i] 1. 尽量不要使用递归。递归可以很优雅和简洁,但是太多的函数调用会造成巨大的开销。
- 避免在循环中使用 sqrt() 函数,因为计算平方根是非常消耗 CPU 的。
一维数组比多维数组更快。所以你可以多想一下将维的方法
浮点数的乘法通常比除法快–使用 val * 0.5 而不是 val / 2.0。
加法运算比乘法更快–使用 val + val + val 而不是 val * 3。puts() 函数比 printf() 函数快,虽然不是很灵活。
如果你还想研究的话就 并行展开,用大数据多次测量比较。用汇编指令说话,比较编译后的汇编差别。以及了解时间周期,便于指出最慢的那个操作。以及背后的原理
(看不懂汇编 逃内存开小:主要是数量级上要小,比如 O(nlogn)->O(n),O(n^2)->O(n) 等。申请内存和释放内存次数尽量少:数据结构中使用内存池就是个好例子。
对于很大的图/树重标号,即按 dfs 序重新标号,可以让内存连续。
减少瓶颈操作次数,比如 LCT/线段树的 pushup,pushdown 是瓶颈,所以应该减少它们的次数。ZKW 线段树正是因为减少了期望次数才变快了
10. dp 的时候精确计算上界,减少状态数。
最后 5 点我是直接加上 ComeIntoPower dalao 的话的,因为我并不怎么会这些操作 还是等我技术再好一点的时候说吧,读者可以自行研究一下 (太难了 ,QAQ 逃
总结一下
首先对于一道题,如果只能想出 O(n^2) 的暴力,那么不妨试试这些优化。而不是着拿到一个题,一眼过去就用暴力+卡常。
优化你的算法最重要,计算机科学的支柱是算法,而不是底层优化。
5 条评论
B_Z_B_Y · 2018年10月25日 4:59 下午
突然觉得 ,手写 stl 还不如 直接用 c++库的 stl,
(自己太弱,老是写出 bug),反正 NOIP 手写,不手写 不是很重要(因为该 T 的还是会 T QvQ)(所以 总的来说算法 研究最重要)wendster · 2018年10月15日 12:07 下午
诡异,超大字体而又超长文章严重搅乱了我的大脑皮层灰质……
B_Z_B_Y · 2018年10月15日 2:41 下午
等我 有时间 改改 QAQ
by · 2018年10月14日 12:18 下午
这篇文章的数学公式好像都没显示出来?
by · 2018年10月14日 12:20 下午
好吧, 我的锅。。。