“程序员浪费了大量的时间来思考或者担心程序中非关键部分的速度,而这些效率的尝试实际上在考虑调试和维护时会产生很大的负面影响。我们应该忘记为了小的性能使用的97%的时间:过早的优化是万恶之源,但我们不应该在这个关键的3%中放弃我们的优化机会。” - Knuth
基础步骤
- 确定项目是否需要优化
- 你的目标是优化什么,CPU、内存或者服务延迟等
- 选择合适的方法
- 选择合适的benchmark
- 在合适的时候停止优化
选择合适的优化方法
Good performance work requires knowledge at many different levels, from system design, networking, hardware (CPU, caches, storage), algorithms, tuning, and debugging. With limited time and resources, consider which level will give the most improvement: it won't always be an algorithm or program tuning.
在不同级别的优化需要自顶向下进行,对于操作系统级别的优化会比表层级别的优化更有效。
Amdahl's Law定理告诉我们要专注于系统的瓶颈,现在存在一个CPU使用率为80%、20%的函数A、B。我们对于B的优化收益远远大于A
程序调优的基本步骤
- 提出一个假设,为什么你的程序很慢。
- 拿出N个解决方案来解决它
- 尝试一切,并保持最快。
- 以防万一。
- 重复。
改变数据结构
以空间换时间
- 额外的数据:比如string中的len字段、双向链表中的前向指针,一个额外的数据往往能有意料之外的效果
- 额外的索引
- 关于元素的额外信息:比如布隆过滤器可以帮助我们快速的返回“未匹配”结果
- 使用缓存
- 注意缓存的大小,太大的缓存会影响GC的效率;大Map引起的gc性能消耗
- 注意线程安全问题
- 缓存不需要很复杂,小小的缓存巨大的收益
- 以真实数据(缓存命中率)为支撑选择合适的驱逐算法
以上描述基本都以空间换来时间,这就涉及到空间、时间消耗问题的平衡;(但一般存储比CPU更便宜一点,小声逼逼)
以时间换空间
- 重新组织数据;消除结构填充、删除额外字段、选择更小的数据类型
- 改变数据结构;简单的数据结构往往需求更少的内存,比如将树替换为切片和线性结构
- 自定义压缩算法;
- proto buffer、Huffman
- 对于压缩数据,你是否需要解压进行处理?如果你想要获取某一个单独数据块,但不想解压所有数据;你可以分块压缩这些数据,并记录这些分块的位置
现代计算机和内存架构使得时空问题变得模糊起来,因为一个索引表可能并不在内存中(虚拟内存),所以有时候每次都重新计算可能比查询索引表更快;这同时会让benchmark的测试变得不再准确,因为开发环境与线上环境的内存架构不同
改变算法
If you know you need random access, don't choose a linked-list. If you know you need in-order traversal, don't use a map
TODO:一个由遍历go map引起的血案
- 掌握各种基本操作的大O时间,能够帮助我们写出“不慢”的程序,这也应该作为我们的基本开发模式。
- 根据问题规模使用相应的算法,经常被忽略的大O问题:1.常数因子2.大O只有在问题规模足够大时才有效,在go的快速排序算法中,会根据情况使用快速排序
func quickSort_func(data lessSwap, a, b, maxDepth int) {
for b-a > 12 {
if maxDepth == 0 {
heapSort_func()
return
}
.....
quickSort_func()
}
if b-a > 1 {
...
insertionSort_func()
}
}
- 随机算法(扩展),“The power of two random choices”在负载均衡中被使用,不要尝试从一组中选择出最好的。随机从一组中选择两个,在这两个中选择最好的
GC
You pay for memory allocation more than once. The first is obviously when you allocate it. But you also pay every time the garbage collection runs.
- 栈分配 vs 堆分配
- 什么会引起堆分配(指针)
- 逃逸分析
- /debug/pprof/heap , and -base
- 限制分配
- 在buffer中为caller传递值,减少分配
- 传递结构体,而不是结构体的指针;使得发生栈分配
- buffer的重用(sync.Pool)
- 使用错误变量而不是errors.New();代码风格vs性能
TODO:其他整理
选择合适的benchmark
- 选择合适的测量指标
- 选择合适的输入