一些常数优化。
参考了宋佳兴的集训队论文。感谢 zzz,如果进集训队了算你一份。
其实啥都不懂,所以可能有很多错的地方。
通用优化方法
循环展开
- int 类型求和
直接写的瓶颈是,指令之间的跳转太多,以及指令之间的依赖关系。
比如 ffor(i,1,n) sum+=a[i]。可以选择把循环进行展开,以及通过某种方式增加并行:
for(int i=0;i<2000;i+=8) {
s0+=a[i],s1+=a[i+1],s2+=a[i+2],s3+=a[i+3];
s0+=a[i+4],s1+=a[i+5],s2+=a[i+6],s3+=a[i+7];
}
sum=s0+s1+s2+s3;
- int 类型加整数
按照 \(4\) 步进行循环展开。
- int 类型前缀和的计算
据说 NOI 应该写 ffor(i,1,n-1) pre[i+1]+=pre[i];。
一种可行的并行:
int sum=0;
for(int i=0;i<2000;i+=4) {
sum+=a[i],a[i]=sum;
sum+=a[i+1],a[i+1]=sum;
sum+=a[i+2],a[i+2]=sum;
sum+=a[i+3],a[i+3]=sum;
}
不想轻易去改马蜂,所以直接手写循环展开把。 /xk
循环融合
把两个循环结合起来,可以大大减少内存的读写次数。
给了一个这样的例子:
处理特殊项之后循环展开,读取内存次数会减少(对于 \(dp_{i-1,*}\) 来说)。
去除分支
使用 __builtin_expect 函数。比如我们写了 if(A),不过 A 是一个几乎不会出现的 corner case,你可以写成 if(__builtin_expect(A,0)),同样定义几乎一定会发生的变量。
编译器会倾向于预测一个指令是否发生。如果不可预测,可能要采取手段。
cmov
据说这个更快。用 min 或 max,或者三目(提前计算好值),类似
int a=...,b=...;
if(a<b) a=b;
位运算
如果不可预测用 & 之类的代替 &&。
使用 __builtin_ctz 遍历会提高枚举 mask 的速度(存疑,效力并不是太明显)。
只使用比较运算不用 if 不会产生分支。
用 long long 代替 pari 之类的东西
就是更快。原因是,long long 存储加载比两个 int 快,而且比较 long long 是没有分支结构的。
内存访问相关
- 内存局部性
让访问尽可能连续。比如预处理 st 表的时候,把小的那一维放在前面。区间 dp 的时候,可以按照 \((l-,k+,r+)\) 的顺序枚举。
- 时间局部性
让适合缓存的数据在一段时间内被反复访问。
比如滚动数组,如果只开一个(使用合适的转移顺序)会更快;要多次访问数组 a 上的一个区间,可以进行分块,每次加载一个块内的数据再跑更快。
- 内存并行
如果地址可以通过简单的算术得到(而不是指针),认为他们是可以并行的,这样更快(比如树状数组和非递归线段树)。
- 树的重标号
想起来 nfls 之前的案例:对树进行重标号之后,从无法通过变为最优解。
对于那种只和父亲有关的树上 DP,可以按照 BFS 序重标号;子树树链相关的,可以考虑 DFS 序重标号。
使用 DFS 序求 LCA 是高贵的、正确的、先进的,而且适合连续访问内存(比如你需要求出一个点到其他所有点的距离。)
STL 相关
-
vector 可以用
reserve分配内存,减少push_back常数; -
emplace_back据说更快; -
vector 的移动使用
A=move(B);。 -
set 的
insert会返回(iterator,bool)的二元组,可以利用一下; -
erase返回的是下一个数的迭代器,通过迭代器删元素是 \(O(1)\) 的; -
insert(it,x)如果 \(x\) 在正确的位置就是 \(O(1)\)(it 之前)。所以插入两个很接近的元素就可以用这个方法降低常数。初始化 set 可以排序然后在 end 之前插入。
同样适用于 map。
priority_queue 常数非常小,积极使用(代替 multiset);__gnu_pbds::gp_hash_table 比 unordered_map 牛,具体用法靠前看。
sort 用匿名函数更快。
计算密集型算法
计数题通用优化
计数题一堆取模对不对,少取几次模更快。
- 提公因式。
- 用加减代替取模。
- 无符号的东西更快。
- 使用较大类型数据进行暂时存储。但是可能存不下。比如是四个元素乘起来,你可以每 \(128\) 轮对 \(128P^4\) 取模。注意你可以通过手法使得不出现取模只有加减。减少分支的手段:

- 减少取模链。就是我特别喜欢写
a*b%MOD*c%MOD*d%MOD这种东西。它不能并行,延迟很高,可以使用分治合并。超长的乘法链(比如阶乘)使用循环展开,并且减少依赖链。
内积优化
很多题目都是内积的形式,代替 FFT 骗分。乘法是少不了的,据说最优的是每 \(16\) 个分成一块,用 u64 暂存,加到一个 u128 里面去。
后面的明天写 /shui

浙公网安备 33010602011771号