之前我优化过一个游戏服务器,游戏服务器的逻辑线程是一个大循环,里面调用tick函数,tick函数里调用了所有需要check timer & do的事情,然后所有需要check timer & do的事情都塞进tick里。
改进:tick里调用了tick50ms、tick100ms、tick500ms,tick1000ms,tick5000ms,然后把需要check timer & do的逻辑根据精度要求塞到不同的tickXXms里去。
减法减少冗余
减少拷贝、零拷贝
减少参数个数(寄存器参数、取决于ABI约定)
减少函数调用次数/层次
减少存储引用次数
减少无效初始化和重复赋值
循环优化这方面的知识很多,感觉一下子讲不完,提几点,循环套循环要内大外小,尽量把逻辑提取到循环外。
提取与循环无关的表达式,尽量减少循环内不必要计算。
循环内尽量使用局部变量。
循环展开是一种程序变换,通过增加每次迭代计算的元素的数量,减少循环的迭代次数。
还有循环分块的骚操作。
防御性编程适可而止有两个流派,一个是完全的不信任,即所有函数调用里都对参数判断,包括判空,有效性检查等,但这样做有几点不好:
第一,它只是貌似更安全,并不是真的更安全。
第二,它稀释代码浓度,淹没关键语句。
第三,如果通过返回值报告错误,则加重了调用者负担,调用者需要添加额外代码检查,不然更奇怪。
第四,重复判断空耗CPU。
第五,埋雷,把本该crash或者暴露的问题埋得更深。
但这种做法大行其道,它有一定的市场和道理。
另一个是界定边界,区分公开接口和内部实现,检查只在模块之间进行,就相当于进园区的时候,门卫会检查你证件,但之后,则不再检查。因为内部实现是受控的安全上下文,开发者应该完全cover住。
我主张防御性编程适可而止,一些著名的开源项目也不会做过多防御,比如linux kernel、NGINX、skynet等,但现实中,软件开发通常多人合作,每个开发者素质不一样,这就是客观现实,所以我也理解前一种做法。
release干净开发过程中,我们会加很多诊断信息,比如我们可能接管内存分配,从而附加额外的首尾部,通过填写magic Num捕获异常或者内存越界,但这些信息应该只用于开发阶段的DEBUG需要,在release阶段应该通过预处理的方式删除掉。
日志分级其实也体现了这种思想,通常有两种做法,一个是定义级别变量,另一个是预处理,预处理干净,但需要重新编译生成image,而变量更灵活,但变量的比较还是有开销的。
不要忽视这些诊断调试信息的开销,牢记不必做的事情绝不做的原则。
慎用递归递归的写法简单,理解起来也容易,但递归是函数调用,有栈帧建立撤销控制跳转的开销,另外也有爆栈的风险,在性能敏感关键路径,优先考虑用非递归版本。
4、编译优化与优化选项inline
restrict
LTO
PGO
优化选项
5、其他优化绑核
SIMD
锁与并发
锁的粒度
无锁编程
Per-cpu data structure & thread local
内存屏障
异构优化/TCO优化
比如用GPGPU、FPGA、SmartNIC来offload原来cpu的任务,TCO优化指的是不以性能优化为单一指标,而是在满足性能条件下以综合成本为优化直播,当然异构也包括主动利用CPU的avx或者其他逻辑单元,这类优化往往编译器不能自动展开(@zrg)
常识和数据CPU拷贝数据一般一秒钟能做到几百兆,当然每次拷贝的数据长度不同,吞吐不同。
一次函数执行如果耗费超过1000 cycles就比较大了(刨除调用子函数的开销)。
pthread_mutex_t是futex实现,不用每次都进入内核,首次加解锁大概耗时4000-5000 cycles左右,之后,每次加解锁大概120 cycles,O2优化的时候100 cycles,spinlock耗时略少。
lock内存总线+xchg需要50 cycles,一次内存屏障要50 cycles。
有一些无锁的技术,比如CAS,比如linux kernel里的kfifo,主要利用了整型回绕+内存屏障。
几个如何? 1. 如何定位CPU瓶颈?CPU是通常大家最先关注的性能指标,宏观维度有核的CPU使用率,微观有函数的CPU cycle数,根据性能的模型,性能规格与CPU使用率是互相关联的,规格越高,CPU使用率越高,但是处理器的性能往往又受到内存带宽、Cache、发热等因素的影响,所以CPU使用率和规格参数之间并不是简单的线性关系,所以性能规格翻倍并不能简单地翻译成我们的CPU使用率要优化一倍。