一些常数优化。
参考了宋佳兴的集训队论文。感谢 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 类型加整数
按照
- 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
循环融合
把两个循环结合起来,可以大大减少内存的读写次数。
给了一个这样的例子:
处理特殊项之后循环展开,读取内存次数会减少(对于
去除分支
使用 __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 的时候,可以按照
- 时间局部性
让适合缓存的数据在一段时间内被反复访问。
比如滚动数组,如果只开一个(使用合适的转移顺序)会更快;要多次访问数组 a 上的一个区间,可以进行分块,每次加载一个块内的数据再跑更快。
- 内存并行
如果地址可以通过简单的算术得到(而不是指针),认为他们是可以并行的,这样更快(比如树状数组和非递归线段树)。
- 树的重标号
想起来 nfls 之前的案例:对树进行重标号之后,从无法通过变为最优解。
对于那种只和父亲有关的树上 DP,可以按照 BFS 序重标号;子树树链相关的,可以考虑 DFS 序重标号。
使用 DFS 序求 LCA 是高贵的、正确的、先进的,而且适合连续访问内存(比如你需要求出一个点到其他所有点的距离。)
STL 相关
-
vector 可以用
reserve
分配内存,减少push_back
常数; -
emplace_back
据说更快; -
vector 的移动使用
A=move(B);
。 -
set 的
insert
会返回(iterator,bool)
的二元组,可以利用一下; -
erase
返回的是下一个数的迭代器,通过迭代器删元素是 的; -
insert(it,x)
如果 在正确的位置就是 (it 之前)。所以插入两个很接近的元素就可以用这个方法降低常数。初始化 set 可以排序然后在 end 之前插入。
同样适用于 map。
priority_queue 常数非常小,积极使用(代替 multiset);__gnu_pbds::gp_hash_table
比 unordered_map
牛,具体用法靠前看。
sort 用匿名函数更快。
计算密集型算法
计数题通用优化
计数题一堆取模对不对,少取几次模更快。
- 提公因式。
- 用加减代替取模。
- 无符号的东西更快。
- 使用较大类型数据进行暂时存储。但是可能存不下。比如是四个元素乘起来,你可以每
轮对 取模。注意你可以通过手法使得不出现取模只有加减。减少分支的手段: - 减少取模链。就是我特别喜欢写
a*b%MOD*c%MOD*d%MOD
这种东西。它不能并行,延迟很高,可以使用分治合并。超长的乘法链(比如阶乘)使用循环展开,并且减少依赖链。
内积优化
很多题目都是内积的形式,代替 FFT 骗分。乘法是少不了的,据说最优的是每 u64
暂存,加到一个 u128
里面去。
后面的明天写 /shui
【推荐】FlashTable:表单开发界的极速跑车,让你的开发效率一路狂飙
【推荐】Flutter适配HarmonyOS 5知识地图,实战解析+高频避坑指南
【推荐】博客园的心动:当一群程序员决定开源共建一个真诚相亲平台
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 为什么PostgreSQL不自动缓存执行计划?
· 于是转身独立开发者
· C#.Net筑基-泛型T & 协变逆变
· dotnet 代码调试方法
· DbContext是如何识别出实体集合的
· 【Cursor保姆级教程】零基础小白从安装到实战,手把手教你玩转AI编程神器!
· MySQL查询执行顺序:一张图看懂SQL是如何工作的
· Cursor 实战万字经验分享,与 AI 编码的深度思考
· 工作流引擎系统-基于橙单(flowable)的系统开发-流程配置举例
· 用 AI 制作超长视频,保姆级教程!