优化一个已有的 Go 程序,提高其性能并减少资源占用,把实践过程和思路整理成文章|青训营

114 阅读2分钟

本文主要介绍程序性能优化来节省资源成本相关的建议。

性能优化

性能优化的前提是满足正确可靠、简洁清晰等质量因素,性能优化需要综合评估,有时候时间效率和空间效率可能对立。

性能优化建议-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一般来说有三种方式:

  1. 手写汇编
  2. llvm
  3. cgo(如果用cgo的方式来调用,会受限于cgo的性能,达不到加速的目的)

总结

本文主要提出了性能优化的多条建议,从Slice、反射,simd,字符串及 atomic 包等方面进行了阐述,避免常见的性能陷阱可以保证大部分程序的性能,同时也要注意,针对普通应用代码,不要一味地追求程序的性能,应当在满足正确可靠、简洁清晰等质量要求的前提下提高程序性能。