Go语言性能优化建议 | 青训营

56 阅读2分钟

性能优化建议

  • 性能优化的前提是满足正确可靠、简洁清晰等质量因素
  • 性能优化是综合评估,有时候时间效率和空间效率可能对立
  • 针对Go语言特性,介绍Go相关的性能优化建议

Slice 预分配内存

尽可能在使用 make 初始化切片时提供容量信息

func PreAlloc(size int){
	data := make([]int,0,size)
    for k:=0;k<size;k++{
        data = append(data,k)
    }
}

TIPS:

  • 切片的本质时一个数组片段的描述
    • 包括数组指针
    • 片段的长度
    • 片段的容量 (不改变内存分配情况下的最大长度)
  • 切片操作并不复制切片只想的元素
  • 创建一个新的切片会复用原来切片的底层数组

Slice 内存释放

  • 在已有的切片基础上创建切片,不会创建新的底层数组
  • 场景:
    • 原切片较大,代码在原切片基础上创建小切片
    • 原底层数组在内存中有引用,得不到释放
      用 Copy( ) 代替 re-slice
//Not Good
func GetLastBySlice(origin []int) []int{
	return originp[len(origin):-2]
}

//Good
func GetLastByCopy(origin []int) []int{
    result := make([]int, 2)
    copy(result, origin[len(origin)-2:])
    return result
}

Map 预分配内存

func BenchmarkNoPreAlloc(b *testing.B) {  
	for i := 0; i < b.N; i++ {  
	NoPreAlloc(10)  
	}  
}  
  
func BenchmarkPreAlloc(b *testing.B) {  
	for i := 0; i < b.N; i++ {  
	PreAlloc(10)  
	}  
}
BenchmarkNoPreAlloc-8            1880264               622.3 ns/op
BenchmarkPreAlloc-8              2959531               412.0 ns/op

分析:

  • 不断向 map 中添加元素会触发 map 的扩容
  • 提前分配好空间可以减少扩容带来的内存拷贝和 Rehash 消耗
  • 根据实际需求提前预估好需要的空间

字符串处理

  1. 使用 strings. Builder
//常见的字符串拼接方式
func Plus(n int, str string) string {  
	s := ""  
	for i := 0; i < n; i++ {  
		s += str  
	}  
	return s  
}  
//使用strings.Builder
func StrBuilder(n int, str string) string {  
	var builder strings.Builder  
	for i := 0; i < n; i++ {  
		builder.WriteString(str)  
	}  
		return builder.String()  
	}
BenchmarkPlus-8                  1842661               671.8 ns/op
BenchmarkStrBuilder-8            4964269               230.4 ns/op

TIPS:

  • 使用 + 拼接性能最差,strings. Builder , bytes. Buffer 相近,strings. Buffer 更快
  • 源码注释也说 strings. Builder 更好
//To build strings more efficiently, see the strings.Builder type
func (b *Buffer) String() string{
	if b == nil{
		//Special case, useful in debugging.
		return "<nil>"
	}
	return string(b.buf[b.off:])
}

//String returns the accumulated string.
func (b *Builder) String() string{
	reutn *(*string)(unsafe.Pointer(&b.buf))
}

空结构体

使用空结构体节省内存

  • 空结构体 struct{}实例不占据任何的内存空间
  • 可作为各种场景下的占位符使用
    • 节省资源
    • 空结构体本身具备很强的语义,即这里不需要任何值,仅作为占位符
func EmptyStructMap(n int) {  
	m := make(map[int]struct{})  
	for i := 0; i < n; i++ {  
		m[i] = struct{}{}  
	}  
}  
  
func BoolMap(n int) {  
	m := make(map[int]bool)  
	for i := 0; i < n; i++ {  
		m[i] = false  
	}  
}
BenchmarkEmptyStructMap-8         124122              8723 ns/op
BenchmarkBoolMap-8                129241              9273 ns/op

TIPS:

  • 实现 Set,可以考虑用 map 来代替
  • 对于这个场景,只需要用到 map 的键,而不需要值
  • 即使将 map 的值设置为最小的 bool 类型,也会多占据一个字节空间

使用atomic

//Atomic
type atomicCounter struct {  
	i int32  
}  
func AtomicAddOne(c *atomicCounter) {  
	atomic.AddInt32(&c.i, 1)  
}  

//Mutex
type mutexCounter struct {  
	i int32  
	m sync.Mutex  
}  
func MutesAddOne(mu *mutexCounter) {  
	mu.m.Lock()  
	mu.i++  
	mu.m.Unlock()  
}
BenchmarkAtomicAddOne-8         45760482                25.20 ns/op
BenchmarkMutesAddOne-8          23814436                54.63 ns/op

TIPS:

  • 锁的实现是通过操作系统来实现,属于系统调用
  • Atomic 操作时通过硬件实现,效率比锁高
  • Sync. Mutex 应该用来保护一段逻辑,不仅仅用于保护一个变量
  • 对于非数值操作,可以使用 atomic. Value,能承载一个 interface{ }

小结

  • 避免常见的性能陷阱可以保证大部分程序的性能
  • 普通应用代码,不要一味地追求程序性能
  • 越高级的性能优化手段越容易出现问题
  • 在满足正确可靠,简介清晰的质量要求前提下提高程序性能