性能优化建议
- 性能优化的前提是满足正确可靠、简洁清晰等质量因素
- 性能优化是综合评估,有时候时间效率和空间效率可能对立
- 针对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 消耗
- 根据实际需求提前预估好需要的空间
字符串处理
- 使用 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{ }
小结
- 避免常见的性能陷阱可以保证大部分程序的性能
- 普通应用代码,不要一味地追求程序性能
- 越高级的性能优化手段越容易出现问题
- 在满足正确可靠,简介清晰的质量要求前提下提高程序性能