这是我参与「第五届青训营 」伴学笔记创作活动的第 4 天
性能优化
string2bytes & bytes2string
这也是两个比较常规的优化手段,核心还是复用对象,减少内存分配。
在go标准库中也有类似的用法gostringnocopy
要注意string2bytes后,不能对其修改。
unsafe.Pointer经常出现在各种优化方案中,使用时要非常小心。这类操作引发的异常,通常是不能recover的。
协程池
绝大部分应用场景,go是不需要协程池的。当然,协程池还是有一些自己的优势:
可以限制goroutine数量,避免无限制的增长。 减少栈扩容的次数。 频繁创建goroutine的场景下,资源复用,节省内存。(需要一定规模。一般场景下,效果不太明显) go对goroutine有一定的复用能力。所以要根据场景选择是否使用连接池,不恰当的场景不仅得不到收益,反而增加系统复杂性
反射
go里面的反射代码可读性本来就差,常见的优化手段进一步牺牲可读性。 而且后续马上就有范型的支持,所以若非必要,建议不要优化反射部分的代码
比较常见的优化手段有:
-
缓存反射结果,减少不必要的反射次数。例如json-iterator
-
直接使用unsafe.Pointer根据各个字段偏移赋值
-
消除一般的struct反射内存消耗go-reflect
-
避免一些类型转换,如interface->[]byte。可以参考zerolog
减小锁消耗
并发场景下,对临界区加锁比较常见。带来的性能隐患也必须重视。常见的优化手段有:
减小锁力度: go标准库当中,math.rand就有这么一处隐患。当我们直接使用rand库生成随机数时,实际上由全局的globalRand对象负责生成。globalRand加锁后生成随机数,会导致我们在高频使用随机数的场景下效率低下。 atomic: 适当场景下,用原子操作代替互斥锁也是一种经典的lock-free技巧。
标准库中sync.map针对读操作的优化消除了rwlock,是一个标准的案例。对它的介绍文章也比较多,不在赘述。
prometheus里的组件histograms直方图也是一个非常巧妙的设计。
一般的开源库,比如go-metrics都是直接在这里使用了互斥锁。指标上报作为一个高频操作,在这里加锁,对系统性能影响可想而知。
参考sync.map里冗余map的做法,prometheus把原来histograms的计数器也分为两个:cold和hot,还有一个hotIdx用来表示哪个计数器是hot。 业务代码上报指标时,用atomic原子操作对hot计数器累加 向prometheus服务上报数据时,更改hotIdx,把原来的热数据变为冷数据,作为上报的数据。然后把现在冷数据里的值,累加到热数据里,完成一次冷热数据的更新替换。