Go语言性能优化建议(一) | 青训营笔记

149 阅读3分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记

Slice

new和make的区别:

二者都是内存的分配(堆上),但是make只用于slice、map以及channel的初始化(非零值);而new用于类型的内存分配,并且内存置为零。所以在我们编写程序的时候,就可以根据自己的需要很好的选择了。 make返回的还是这三个引用类型本身;而new返回的是指向类型的指针。 1、尽量初始化容量信息 slice由一个指向底层数组的指针、大小和容量构成,如图(图源来自b站幼麟实验室)

image-20220417194230180.png 如果一个slice未进行初始化,那么它的容量为0,每次往此slice中append元素时,都会进行扩容,扩容规则如下(图源来自b站幼麟实验室) image-20220417194946795.png 所以使用slice时,如实现知道它的大致容量,应该初始化它的容量信息,避免内存发生拷贝 2、切片操作导致大内存未释放 因为切片操作并不复制切片指向的元素,创建一个新的切片会复用原来切片的底层数组(图源来自b站幼麟实验室) image-20220417194732967.png 当一个小切片一直复用着大底层数组时,大底层数组一直得不到释放,就会占很大内存空间。 解决方案:使用copy替代re-slice

Map

map不支持并发读写,只能并发读

对于需要并发读写map的场景,常见的解决方案如下:

  • map + sync.RWMutex
  • 采用 sync.map,实现是小粒度的锁+读写分离+原子操作

超出容量时会自动扩容,但尽量提供一个合理的初始值

map有翻倍扩容和等量扩容2种:

翻倍扩容:当该定义下的装载因子达到 6.5 时便需要触发 map 的扩容,此时再不扩容,就会造成hash冲突严重,使map查找速率下降

等量扩容:若溢出桶过多也会触发 map 的扩容, 这是基于这样的考虑, 向 map 中插入大量的元素, 哈希桶将逐渐被填满, 这个过程中也可能创建了一些溢出桶, 但此时装载因子并没有超过设定的阈值, 然后对这些 map 做删除操作, 删除元素之后, map 中的元素数目变少, 使得装载因子降低, 而后又重复上述的过程, 最终使得整体的装载因子不大, 但整个 map 中存在了大量的溢出桶, 因此当溢出桶数目过多时, 即便没有达到装载因子 6.5 的阈值也会触发扩容,其目的是整理map桶。

字符串处理

使用+拼接性能最差,strings.Builder和bytes.Buffer相近,但strings.Builder最快

1、字符串在go语言中是不可变类型,占用内存大小是固定的,使用+会都会重新分配内存

2、strings.Builder和bytes.Buffer的底层都是是同[]byte数组,但strings.Builder只有一次内存分配,而bytes.Buffer需要2次

3、在预知字符串长度时,可以预分配长度来进一步提升性能

空结构体

1、使用空结构体来进行通信

package main

import (
	"fmt"
	"os"
	"time"
)
func main() {
	abort := make(chan struct{})
	go func() {
		os.Stdin.Read(make([]byte, 1)) // read a single byte
		abort <- struct{}{}
	}()

	fmt.Println("Commencing countdown.  Press return to abort.")
	select {
	case <-time.After(10 * time.Second):
		// Do nothing.
	case <-abort:
		fmt.Println("Launch aborted!")
		return
	}
	launch()
}
func launch() {
	fmt.Println("Lift off!")
}

2、使用空结构体来实现set

atomic包

锁的实现是由操作系统来实现,属于系统调用

atomic操作通过硬件实现,效率比锁高