本文主要介绍程序性能优化来节省资源成本相关的建议。
性能优化
性能优化的前提是满足正确可靠、简洁清晰等质量因素,性能优化需要综合评估,有时候时间效率和空间效率可能对立。
性能优化建议-Slice
func Noapply(size int)
{ data := make([]int, 0)
for k := 0; k < size; k++
{ data = append(data, k) } }
func Preapply(size int)
{ data := make([]int, 0, size)
for k := 0; k < size; k++
{ data = append(data, k)} }
在已有切片的基础上进行切片,不会创建新的底层数组。因为原来的底层数组没有发生变化,内存会一直占用,直到没有变量引用该数组
因此很可能出现这么一种情况,原切片由大量的元素构成,但是我们在原切片的基础上切片,虽然只使用了很小一段,但底层数组在内存中仍然占据了大量空间,得不到释放
log-函数名称行号的获取
在runtime中,函数行号和函数名称的获取分为两步:
runtime回溯goroutine栈,获取上层调用方函数的的程序计数器(pc)。
根据pc,找到对应的funcInfo,然后返回行号名称
经过pprof分析。第二步性能占比最大,约60%。针对第一步,我们经过多次尝试,并没有找到有效的办法。但是第二步很明显,我们不需要每次都调用runtime函数去查找pc和函数信息的,我们可以把第一次的结果缓存起来,后面直接使用。这样。第二步约60%的消耗就可以去掉。
m sync.Map
)
func Caller(skip int)(pc uintptr, file string, line int, ok bool){
rpc := [1]uintptr{}
n := runtime.Callers(skip+1, rpc[:])
if n < 1 {
return
}
var (
frame runtime.Frame
)
pc = rpc[0]
if item,ok:=m.Load(pc);ok{
frame = item.(runtime.Frame)
}else{
tmprpc := []uintptr{
pc,
}
frame, _ = runtime.CallersFrames(tmprpc).Next()
m.Store(pc,frame)
}
return frame.PC,frame.File,frame.Line,frame.PC!=0
字符串
常见的字符串拼接方式
字符串在 Go 语言中是不可变类型,占用内存大小是固定的,当使用 + 拼接 2 个字符串时,生成一个新的字符串,那么就需要开辟一段新的空间,新空间的大小是原来两个字符串的大小之和
strings.Builder,bytes.Buffer 的内存是以倍数申请的
strings.Builder 和 bytes.Buffer 底层都是 []byte 数组,bytes.Buffer 转化为字符串时重新申请了一块空间,存放生成的字符串变量,而 strings.Builder 直接将底层的 []byte 转换成了字符串类型返回
atomic 包
实际工程中,一定会遇到多线程编程的场景,比如实现一个多线程共用的计数器,有多种方式保证技术准确且线程安全,他们的性能存在差异。
type atomicCounter struct {
i int32
}
func AtomicAddOne(c *atomicCounter) {
atomic.AddInt32(&c.i, 1)
}
// 互斥锁
type mutexCounter struct {
i int32
m sync.Mutex
}
func MutexAddOne(c *mutexCounter) {
c.m.Lock()
c.i++
c.m.Unlock()
}
反射
go里面的反射代码可读性本来就差,常见的优化手段进一步牺牲可读性。 而且后续马上就有范型的支持,所以若非必要,建议不要优化反射部分的代码
比较常见的优化手段有:
缓存反射结果,减少不必要的反射次数。例如json-iterator 直接使用unsafe.Pointer根据各个字段偏移赋值 消除一般的struct反射内存消耗go-reflect 避免一些类型转换,如interface->[]byte。可以参考zerolog
simd
首先,go链接器支持simd指令,但go编译器不支持simd指令的生成。
所以在go中使用simd一般来说有三种方式:
- 手写汇编
- llvm
- cgo(如果用cgo的方式来调用,会受限于cgo的性能,达不到加速的目的)
总结
本文主要提出了性能优化的多条建议,从Slice、反射,simd,字符串及 atomic 包等方面进行了阐述,避免常见的性能陷阱可以保证大部分程序的性能,同时也要注意,针对普通应用代码,不要一味地追求程序的性能,应当在满足正确可靠、简洁清晰等质量要求的前提下提高程序性能。