性能优化
在满足正确性、可靠性、健壮性、可读性等质量因素的前提下,设法提高程序的效率
slice 预分配内存
在尽可能的情况下,在使用 make() 初始化切片时提供容量信息,特别是在追加切片时
原理
切片本质是一个数组片段的描述,包括了数组的指针,这个片段的长度和容量(不改变内存分配情况下的最大长度)
切片操作并不复制切片指向的元素,创建一个新的切片会复用原来切片的底层数组,因此切片操作是非常高效的
切片有三个属性,指针(ptr)、长度(len) 和容量(cap)。append 时有两种场景:
- 当 append 之后的长度小于等于 cap,将会直接利用原底层数组剩余的空间
- 当 append 后的长度大于 cap 时,则会分配一块更大的区域来容纳新的底层数组
因此,为了避免内存发生拷贝,如果能够知道最终的切片的大小,预先设置 cap 的值能够获得最好的性能
另一个陷阱:大内存得不到释放
在已有切片的基础上进行切片,不会创建新的底层数组。因为原来的底层数组没有发生变化,内存会一直占用,直到没有变量引用该数组
因此很可能出现这么一种情况,原切片由大量的元素构成,但是我们在原切片的基础上切片,虽然只使用了很小一段,但底层数组在内存中仍然占据了大量空间,得不到释放
推荐的做法,使用 copy 替代
re-slice
map 预分配内存
- 原理
- 不断向 map 中添加元素的操作会触发 map 的扩容
- 根据实际需求提前预估好需要的空间
- 提前分配好空间可以减少内存拷贝和 Rehash 的消耗
使用 strings.Builder
常见的字符串拼接方式
+strings.Builderbytes.Buffer
strings.Builder 最快,bytes.Buffer 较快,+ 最慢
原理
字符串在 Go 语言中是不可变类型,占用内存大小是固定的,当使用 + 拼接 2 个字符串时,生成一个新的字符串,那么就需要开辟一段新的空间,新空间的大小是原来两个字符串的大小之和
strings.Builder,bytes.Buffer的内存是以倍数申请的strings.Builder和bytes.Buffer底层都是[]byte数组,bytes.Buffer转化为字符串时重新申请了一块空间,存放生成的字符串变量,而strings.Builder直接将底层的[]byte转换成了字符串类型返回
使用空结构体节省内存
空结构体不占据内存空间,可作为占位符使用
比如实现简单的 Set
- Go 语言标准库没有提供 Set 的实现,通常使用 map 来代替。对于集合场景,只需要用到 map 的键而不需要值
map[key] = struct{}{}
- Go 语言标准库没有提供 Set 的实现,通常使用 map 来代替。对于集合场景,只需要用到 map 的键而不需要值
使用 atomic 包
原理
- 锁的实现是通过操作系统来实现,属于系统调用,
atomic操作是通过硬件实现的,效率比锁高很多 sync.Mutex应该用来保护一段逻辑,不仅仅用于保护一个变量- 对于非数值系列,可以使用
atomic.Value,atomic.Value能承载一个interface{}
总结
- 避免常见的性能陷阱可以保证大部分程序的性能
- 针对普通应用代码,不要一味地追求程序的性能,应当在满足正确可靠、简洁清晰等质量要求的前提下提高程序性能