GPM调度模型
想要进行性能优化首先要了解最基础的底层模型
- 一个 G 就是一个 goroutine,在 runtime 中通过类型 g 来表示。当一个 goroutine 退出时,g 对象会被放到一个空闲的 g 对象池中以用于后续的 goroutine 的使用(减少内存分配开销)。
- 一个 M 就是一个系统的线程,系统线程可以执行用户的 go 代码、runtime 代码、系统调用或者空闲等待。在 runtime 中通过类型 m 来表示。在同一时间,可能有任意数量的 M,因为任意数量的 M 可能会阻塞在系统调用中。(当一个 M 执行阻塞的系统调用时,会将 M 和 P 解绑,并创建出一个新 的 M 来执行 P 上的其它 G。)
- 最后,一个 P 代表了执行用户 go 代码所需要的资源,比如调度器状态、内存分配器状态等。在runtime 中通过类型 p 来表示。P 的数量精确地(exactly)等于 GOMAXPROCS。一个 P 可以被理 解为是操作系统调度器中的 CPU,p 类型可以被理解为是每个 CPU 的状态。调度器的工作是将一个 G(需要执行的代码)、一个 M(代码执行的地方)和一个 P(代码执行所需要 的权限和资源)结合起来。当一个 M 停止执行用户代码的时候(比如进入阻塞的系统调用的时候),就需要把它的 P 归还到空闲的 P 池中;为了继续执行用户的 go 代码(比如从阻塞的系统调用退出 的时候),就需要从空闲的 P 池中获取一个 P。 所有的 g、m 和 p 对象都是分配在堆上且永不释放的,所以它们的内存使用是很稳定的。得益于此 ,runtime 可以在调度器实现中避免写屏障(垃圾回收时需要的一种屏障,会带来一些性能开销)。
如何优化
slice 预分配内存优化
map 预分配内存优化
使用 strings.Builder 而不是 bytes.Buffer
函数中尽可能使用值而不是指针
● 使用指针会使逃逸分析将变量分配在堆上
● 分配在栈上的对象会随着栈销毁而回收,不会给 gc 带来压力
● 在栈上进行小对象拷贝的性能很好,比分配对象在堆上要好非常多
● 这个规则适用于函数接受者(receiver):指针仅仅应该用来表示“可修改权”,只有当方法会修改对象,并且修改后的值需要在方法外感知到时,才应该使用指针
● 适用于 slice 类型,如非必要,slice 不要包含指针。
map 存值而不是指针
使用 struct{} 优化
● 可以和普通 struct 一样操作
● 不占用空间
● 指向同一个内存地址(runtime.zerobase,编译器特殊优化)
使用 atomic 优化
单纯使用锁是调度系统锁
使用原子化操作内存更低
使用不带缓冲区的 channel
一些工具
● go tool pprof
● go tool trace
● go build -gcflags=”-m”
● GODEBUG=”gctrace=1”
● go get benchmark