性能优化建议丨 青训营笔记
这是我参与「第五届青训营」笔记创作活动的第5天。
一、本堂课重点内容
本堂课在满足正确性、可靠性、健壮性、可读性等质量因素的前提下,为提高程序的效率提出了几点性能优化建议,有效提升代码执行效率,同时提升代码的可用性、可维护性和可扩展性。
二、详细知识点介绍
性能评估工具 benchmark
go test -bench=. -benchmem
-bench=. 参数指定要运行的测试的名称,在这种情况下是 ".",表示当前目录中的所有测试。
-benchmem 参数指示 go test 命令显示内存分配的详细信息,这是一个非常有用的工具,可以帮助你评估代码的内存使用情况。
slice 预分配内存
- 在尽可能的情况下,在使用 make() 初始化切片时提供容量信息,特别是在追加切片时
对比两种情况的性能表现,左边是没有预分配内存,右边是预分配内存,结果中可以看出执行时间相差很多,预分配只有一次内存分配。
-
原理
- 切片本质是一个数组片段的描述,包括了数组的指针,这个片段的长度和容量(不改变内存分配情况下的最大长度)
- 切片操作并不复制切片指向的元素,创建一个新的切片会复用原来切片的底层数组,因此切片操作是非常高效的
type slice struct {
array unsafe.Pointer
len int
cap int
}
-
切片有三个属性,指针(ptr)、长度(len) 和容量(cap)。append 时有两种场景:
- 当 append 之后的长度小于等于 cap,将会直接利用原底层数组剩余的空间
- 当 append 后的长度大于 cap 时,则会分配一块更大的区域来容纳新的底层数组
-
因此,为了避免内存发生拷贝,如果能够知道最终的切片的大小,预先设置 cap 的值能够获得最好的性能
map 预分配内存
- 原理
- 不断向 map 中添加元素的操作会触发 map 的扩容
- 根据实际需求提前预估好需要的空间
- 提前分配好空间可以减少内存拷贝和 Rehash 的消耗
使用 strings.Builder
-
常见的字符串拼接方式
- +
- strings.Builder
- bytes.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 的键而不需要值
使用 atomic 包
-
原理
- 锁的实现是通过操作系统来实现,属于系统调用,atomic 操作是通过硬件实现的,效率比锁高很多
- sync.Mutex 应该用来保护一段逻辑,不仅仅用于保护一个变量
- 对于非数值系列,可以使用 atomic.Value,atomic.Value 能承载一个 interface{}
三、课后个人总结
- 为保证大部分程序的性能,需避免常见的性能陷阱
Go语言中常见的性能陷阱:
- 内存泄露:造成内存泄露的原因包括使用了不正确的指针,未释放不再使用的内存等。
- 频繁的内存分配:在频繁分配内存时会导致内存碎片化,从而影响性能。
- 不必要的数据拷贝:在数据处理过程中,如果有大量不必要的数据拷贝,将会降低性能。
- 遍历大数据集时的性能问题:在遍历大数据集时,如果使用不当,将会导致性能问题。
- 使用了过多的锁:在多线程环境下,如果使用了过多的锁,将会导致性能问题。
- 循环嵌套过深:在代码中过深的循环嵌套会导致性能问题。
- 使用不当的缓存:在使用缓存时,如果不适当使用,将会导致性能问题。
- 使用不当的正则表达式:在使用正则表达式时,如果不适当使用,将会导致性能问题。
- 针对普通应用代码,不要一味地追求程序的性能,应当在满足正确可靠、简洁清晰等质量要求的前提下提高程序性能