这是我参与「第五届青训营」伴学笔记创作活动的第 5 天
性能优化的前提是满足正确可靠、清晰简洁等质量因素。性能优化是综合评估,有时候时间效率和空间效率可能对立。这篇笔记根据 Go 语言的相关特性,记录 Go 语言的性能优化建议。
性能表现通过实际数据来衡量,借助 Go 提供的基准性能测试获得实际数据。
Slice预分配内存
尽可能在使用make初始化切片时提供容量信息。
- 没有提供初始的容量信息:
func NoPreAlloc(size int) {
// 初始空间大小为 0 , 无预留的空间
data := make([]int, 0)
for k := 0; k < size; k++ {
data = append(data, k)
}
}
- 提供了初始容量信息:
func PreAlloc(size int) {
// 初始空间大小为 0 , 预留的空间大小为 size
data := make([]int, 0, size)
for k := 0; k < size; k++ {
data = append(data, k)
}
}
切片本质是一个数组片段的描述,类似如下:
type slice struct {
// 指向数组的指针
array unsafe.Pointer
// 片段的长度
len int
// 片段的容量(不改变内存分配情况下的最大长度)
cap int
}
当容量已满时,再加入元素就会扩容,扩容需再次申请内存创建新数组且要复制元素到新数组,因此预先提供容量信息可以减少扩容的次数,从而减少内存分配的次数,且可以减少复制元素的时间。
另外,使用slice还要注意大内存未释放的问题。在已有切片基础上创建切片,不会创建新的底层数组:
func GetLastBySlice(origin []int) []int {
return origin[len(origin) - 2 : ]
}
- 原切片较大,此函数在原切片的基础上创建小切片。
- 调用此函数得到的切片使用的底层数组仍然是原来的数组,因此原来的数组仍然被引用,原来较大的内存得不到释放。
使用copy函数即可创建一个新的小内存的数组,原来的大内存即可得到释放:
func GetLastByCopy(origin []int) []int {
res := make([]int, 2)
copy(res, origin[len(origin)-2:])
return res
}
Map预分配内存
同理,Map在创建的时候预分配内存也可以提高性能。
- 不断向 Map 中添加元素的操作会触发 Map 扩容。
- 提前分配好空间可以减少扩容次数,减少扩容时的内存拷贝和重新计算哈希值。
- 因此建议根据实际需求提前预估好所需空间。
字符串拼接
使用strings.Builder或bytes.Buffer代替+进行字符串拼接:
func Plus(n int, str string) string {
s := ""
for l := 0; l < n; l++ {
s += str
}
return s
}
func StrBuilder(n int, str string) string {
var builder strings.Builder
for l := 0; l < n; l++ {
builder.WriteString(str)
}
return builder.String()
}
func ByteBuffer(n int, str string) string {
// Tips:三种创建结构体对象的方式
// go 中 new(T) 会为类型为 T 的对象分配已置零的内存空间
// buf := new(bytes.Buffer)
// buf := bytes.Buffer{}
var buf bytes.Buffer
for l := 0; l < n; l++ {
buf.WriteString(str)
}
return buf.String()
}
- 字符串在 Go 中是不可变类型,占用内存大小是固定的。
- 使用
+每次都会重新分配内存。 strings.Builder和bytes.Buffer底层都是[]byte数组,采取内存扩容策略,不用每次拼接都重新分配内存。string.Builder最快,bytes.Buffer稍慢一些,+最慢。Builder稍快一些的原因主要在于最后return xxx.String()的时候逻辑的比Buffer稍快一些。
string.Builder和bytes.Buffer在已知字符串长度的情况下,还可以通过预分配进一步提升性能。通过grow进行预分配:
func PreStrBuilder(n int, str string) string {
var builder strings.Builder
builder.Grow(n * len(str)) // 预分配
for l := 0; l < n; l++ {
builder.WriteString(str)
}
return builder.String()
}
func PreByteBuffer(n int, str string) string {
var buf bytes.Buffer
buf.Grow(n * len(str))
for l := 0; l < n; l++ {
buf.WriteString(str)
}
return buf.String()
}
空结构体
- 使用空结构体可以节省内存。
- 空结构体
struct{}实例不占据任何的内存空间。 - 可以作为占位符使用,节省资源。
例如:Go 中没有单独的 Set ,要使用 Set 都是通过只使用 Map 的 Key 来实现 Set 的效果,这样就可以使用空结构体作为 Value 的占位符,除了空结构体以外,使用其他类型作为 Value 都会多占据内存空间。
func EmptyStructMap(n int) {
m := make(map[int]struct{})
for i := 0; i < n; i++ {
m[i] = struct{}{}
}
}
atomic 包
多线(协)程并发安全时,使用atomic包比加锁性能要好很多。
// 使用 atimic 包
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()
}
- 锁的实现是通过操作系统来实现的,属于系统调用。
atomic操作是通过硬件实现的,效率比锁高。sync.Mutex应该用来保护一段逻辑,而不应该仅仅用于保护一个变量。- 对于非数值操作,可以使用
atomic.Value,能承载一个interface{}。