大家好,我是不会写代码的纯序员——Chunel Feng。在日(tiao)常(cao)开(mian)发(shi)过程中,性能优化是一个绕不开的话题。同样功能的代码,不同层次的程序员写出来,性能可能会千差万别。
性能优化问题,设计到计算机科学的方方面面,不同行业的程序员,关注点也可能不尽相同。于是,抽了一个周末的时间,约上了技术交流群里的童鞋,一起讨论了一些优化相关的问题。在这里做一个总结和整理,供大家今后用到的时候,参考对照。主要包含以下几点:
1,数据结构
2,并发编程
3,编程技巧
4,缓存和索引
5,加速计算
6,编译原理
7,零拷贝技术
8,相似计算
9,热点问题排查和定位工具
如有未提及的内容或领域,也欢迎各位大佬随时随地来指教和补充。
需要说明的是,这次讨论主要是focus在程序本身性能的提升。所以,并没有设计到 分布式架构设计、数据倾斜调优、数据库范式和分库分表,或加redis缓存 等架构层面的内容。类似的话题,大家有兴趣的话,今后可以单独抽时间讨论。
一、数据结构 |
- 数据结构层面,主要是配合算法选择适合的数据结构,比如 std::map(TreeMap)和std::unordered_map(HashMap)的区别和使用场景。
- 多刷刷Leetcode总是有好处的,至少在编程的过程,基本的数据结构不能选错。
- 各种常见的搜索、排序算法,也是强依赖数据结构设计的。
- C++17中,添加了部分并行stl,可以用于加速。数据量大的情况下,推荐使用。
二、并发编程 |
2.1 线程角度
多线程的优化,主要是围绕在线程数选择、减少线程切换耗时、增加缓存命中率、提高时间片利用率、合理使用同步互斥机制 几个方面
- 各种锁合理使用(乐观锁,互斥锁,读写锁,可重入锁,线段锁,定时锁,自旋锁 等)
- 各种同步方式(临界区,互斥锁,信号量 等)
- 原子变量(atomic变量)
- 无锁编程(cas技术)
- 线程池(批量处理,增加负载,work-steal)
- yield,让出时间片,避免cpu空跑
- tbb库,openmp库 等多线程库
推荐文章:C++线程池优化系列1~5
2.2 协程角度
协程是可以上游程序控制启、停的函数,多用于高并发网络后台服务中,跟IO复用技术一起联动。
注:大家都没有在工作中用过(一脸懵逼),也没写Go语言的。今后有机会的时候,会再讨论。
三、编程技巧 |
这个也涉及到日常开发的方方面面,列举几个常见的优化思路:
- for循环的处理,大小循环的时候,大循环尽量在内部。可以的话,考虑循环展开
- 尽可能通过位移运算,来代替除法运算
- 合理的使用goto来实现程序加速
- 减少用户态和内核态之间的切换,减少段页切换,减少cache miss次数
- 调整matrix或者vector的计算顺序,从而达到减少运算次数的目的
例:matA 是[10,10]矩阵,matB是[10,5]矩阵,matC是[5,1]矩阵。则计算matA * matB * matC的时候,可以使用 matA * (matB * matC)来减少运算次数。
四、缓存和索引 |
缓存和索引都是加速程序执行的方法,被用在各个领域。比如cpu的三级缓存,tcmalloc的三级缓存,路由寻址的路由信息缓存,LSM顺序写入缓存,LRU缓存等。各种不同领域的数据库,也有各种不同的索引设计。
- cpu执行的时候,有多级缓存。合理设计结构和逻辑,出发缓存可以加速
- 优化数据处理顺序,可以提到缓存命中率(非玄学)
- 为了提高缓存命中率,还可以考虑重新设计数据结构。比如在合理的时候,使用数组代替链表。或者把关系紧密的内容,放到一个簇下
- 索引结构更不必说,要了解不同索引的异同点和适用的场景。根据数据量、优化方向和当前瓶颈(比如时间复杂度、空间复杂度)来选择适当的索引结构。
例:同样是索引,为什么mongoDB选择B树,而mysql选择B+树,HBase选择LSM树,而Milvus(向量数据库)选择(之一)KD树
五、加速计算 |
5.1 并行计算
常见库包括:eigen(强烈推荐),sse,neon,openblas,avx的使用
5.2 异构计算
常见的思路:gpu,fpga。这部分有机会的话,下次专门找人分享
六、编译原理 |
- if分支预测,likely操作
- 设计数据结构的时候,注意内存对齐,可以理论上减少运行时候的寻址次数
- 热点函数使用汇编语言重写
- tcmalloc等库,替代传统的malloc方式。分配内存的时候,根据分块的大小,设计三层存储结构
七、零拷贝技术 |
常用于网络通信,减少用户态和内核态之间的切换,减少数据在cpu中的copy次数。推荐阅读文章:一文带你彻底了解,零拷贝Zero-Copy技术
进行IO的时候,可以考虑攒批发送/接受,而不是单条处理。常用的内容:dma,mmap、sendfile、spice等。
八、相似计算 |
主要指的是,当需要计算出一个准确的结果,需要非常大量的计算,而且在对结果的精确度要求不是非常高的情况下使用。
比如,海量数据的TopK查询任务,很多就可以退化成查询近似TopK任务,或者使用ANN查询来替代KNN查询。这种技术,被广泛运用于人工智能、向量检索等领域。
- 降维计算算法:pca、svd、pq等
- 相似查询算法:hnsw,annoy等
九、热点问题排查和定位工具 |
要优化程序执行效率,第一步就是发现执行过程中的热点问题。大多数情况下,需要依靠专门的工具采集信息,并且将性能问题可视化。比如:火焰图、调用链路耗时分布图,cpu cache命中次数,段页切换次数等。
常见工具:gpref,valgrind,profiling(CLion内置,可一键生成火焰图),SLS(特别适用于分布式场景,强烈推荐),vld,Arthas(阿里出品,Java程序性能分析神器),apiMonitor,permon(windows自带性能分析工具)
本章小节 |
这篇文章中,我们主要整理了一些日常的开发过程中,对程序执行优化执行可能有帮助的一些问题。有些问题,在文章的最后也是需要说明的。
我们平日可能在一些博客和文章中,看到的一些优化思路,比如用“加加i”和“i加加”的性能对比,又比如我们上面提到的添加分支预测(likely)等,其实在程序编译的过程中,编译器会默默的帮忙做掉很多。而且,一般情况下,大部分程序员的优化思路和能力,都不足以跟编译时开个O2、O3优化对比。
最终优化的方案和结果,真心不能想当然。需要根据手头实际任务,设计周密的方案多次验证才行。验证的过程,还需要考虑各方面(甚至会有自己完全不了解的领域)可能存在的影响。但是,一些思路还是有必要了解一下的,一方面是可以少踩坑,还有一点,这些思路很有可能也会触类旁通的用在其他一些相关领域。
本文的内容比较笼统,主要是提供了多个方面的思路的整理,没有给出具体的案例和优化前后对比。平时日常还要搬砖,的确没时间搞。大家可以参考一下思路,具体用的到的时候,还需要带入实际环境验证。今后也可能会针对个别特定的点,做一些相关的实验和测试,进行进一步深入的学习和了解。
最后,欢迎大家加我的个人微信,方面今后随时联系。也请各位大佬多多指教。
[2021.12.17 by Chunel]
个人信息
微信: ChunelFeng
邮箱: chunel@foxmail.com
个人网站:www.chunel.cn
github地址: https://github.com/ChunelFeng