Go语言性能优化 | 青训营笔记

252 阅读5分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 4 天

简介

性能

怎么评估代码性能

Go语言提供了基准性能测试的 benchmark 工具

go test -bench=. -benchmen

性能优化建议

slice 预分配内存

使用基准测试结果说明差距 执行时间三分之一 内存分配次数由8次减小为1次

原理: 底层数据结构:数组片段的描述

type slice struct {
    array unsafe.Pointer  // 底层数组的指针
    len int               // 长度
    cap int               // 容量
}

当切片中元素个数大于切片大小时会发生扩容操作。扩容操作花费一定时间,浪费性能,为了节省这部分性能开销,如果知道具体容量,就应该预先设置容量大小。

陷阱

切片中在创建小的切片,小的切片不会复制大切片的底层数组,而是选择公用一个底层数组,使用新的指针指向合适的位置。 但是当这样一个场景,已经存在大的切片 a := [1:10000]int,这时候使用 a[1:3] 创建一个小的切片,这个切片会引用大的切片的底层数组(该数组的大小10000),进行垃圾回收的时候,会分析引用关系,因为小的切片这2个int的空间,10000int的数组空间被引用着,不能释放。这就造成系统资源的极大浪费。

解决方案

使用 copy 代替 re-slice

// 使用re-slice
func GetLastBySlice(origin []int) []int {
    return origin[len(origin)-2:]
}

// 使用copy
copy(新切片,原始数组要拷贝的部分)
func GetLastByCopy(origin []int) []int {
    result := make([]int, 2)
    copy(result, origin[len(origin)-2:])
    return result
}

// test
func testGetLast(t *testing.T, f func([]int) []int) {
    result := make([][]int, 0)
    for k := 0; k < 100; k++ {
        origin := generateWithCap(128 * 1024)          //1M
        result = append(result, f(origin))            // 
    }
    printMem(t)
    _ = result
}


// 使用 go test -run=. -v 命令查看结果

Map预分配内存

基准测试验证结论

底层

不断往Map中put元素,会引发扩容,如果提前分配,可以减少rehash、分配内存的开销。

字符串处理

三种拼接工具

循环中不使用 + 拼接字符串

  • strings.Builder
  • strings.Buffer
  • + 拼接

原理 go语言中字符串都是不可变类型

使用➕进行拼接时,每次都会分配新的内存空间。 strings.Builder 与 strings.Buffer 底层都是byte数组,可以直接append操作,不需要每次分配内存空间。等到所有附加操作完成,生成stirng字符串时,分配一次内存空间就好。

strings.Builder 比 strings.buffer 性能更好的原因

// strings.Buffer 部分源码
func (b *Buffer) String() string{
    if b == nil {
        return "<nil>"
    }
    // 申请新的内存创建string
    return string(b.buf[b.off:])
}

// strings.Builder 部分源码
func (b *Builder) String() string {
    // 直接将byte数组所在的空间指针类型强制转换为stirng类型然后*Pointer取值返回
    return *(*string)(unsafe.Pointer(&b.buf))
}

进一步优化 知道字符串拼接后的容量的话,可以在stirng.Builder / strings.Buffer 中预先给byte数组进行预分配大小,进一步提升性能。

使用空结构体节省内存

空结构体特点

  • 不占用内存
struct {}

作用 节省资源 作为占位符

示例 空结构体作为Map的value的占位符来实现Set Set只用到key,用不到value。将value定义为结构体的话,就不会占用内存,从而节省空间。

如果没有空结构体,那么用其他类型,即使是布尔值,也会多占一个字节。

正确使用 atomic 包

选择

  • sync.Mutex 应该用来保护一段逻辑
  • atomic 用来保护单个变量
  • 非数值操作,可以使用atomic.Value,能承载一个interface{}

atomic包的效率比加锁的效率好很多

原理

锁是通过操作系统实现的,属于系统调用。 atomic通过硬件实现,效率好。

总结

  • 刚开始时,避开常见性能陷阱就好。
  • 优化要保证程序的正确性、可靠性、简洁清晰
  • 越高级的优化,越容易出问题
  • 普通程序,用不到太过分的优化,不一定追求极致性能。

性能优化原则

  • 依靠数据不是猜测
  • 要定位最大瓶颈而不是细枝末节
  • 不要过早优化
    • 业务的性能瓶颈出现后,再进行优化
  • 不要过度优化
    • 优化手段激进的调优手段更容易出现问题

性能优化工具

pprof

功能简介

排查实战

CPU

直接看任务管理器

结果采样(命令行)
go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10"
top 命令

查看cpu性能分析报告

结果

image.png

解释

  • flat 当前函数的本身的执行耗时
  • flat% flat 占 cpu 总时间的比例
  • sum% 上面每一行的 flat% 综合
  • cum 指当前函数本身加上其调用函数的总耗时
  • cum% cum 占 CPU 总时间的比例

Flat == Cum, 当前函数没有调用其他函数

Flat == 0, 函数中只有其他函数的调用

list命令 分析
list 正则表达式

根据正则表达式查找代码

示例

image.png

image.png

web命令 可视化
web

可视化 / 显示调用关系 image.png

可视化分析
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/cpu"

内存 **

可视化分析
go tool pprof "http://localhost:6060/debug/pprof/heap
四种内存
  • alloc_objects 程序累计申请的对象数
  • alloc_space 程序累计申请的内存大小
  • inuse_objects 程序当前持有的对象数
  • inuse_objects 程序当前占用的内存大小

image.png

阻塞

go tool pprof "http://localhost:6060/debug/pprof/block

协程

go tool pprof "http://localhost:6060/debug/pprof/goroutine

适合火焰图查看

火焰图
  • 从上到下代表调用顺序
  • 一个小块代表一个函数,小块的长度代表占用cpu的时间长短。
  • 图是动态的,可以点击小块进行分析

go tool pprof "http://localhost:6060/debug/pprof/mutex

通用分析流程

  1. 流程图里分析找到占用最大资源的函数
  2. 通过source找到具体行
  3. 注释掉具体行后运行。

采样过程与原理

性能评估工具

Go基准测试

示例

package locks1

import "testing"

func BenchmarkFib(b *testing.B) {
	for n := 0; n < b.N; n++ {
		Fib(10)
	}
}

func Fib(n int) int {
	if n < 2 {
		return n
	}
	return Fib(n-1) + Fib(n-2)
}

命令行中使用 go test -bench=. -benchmem 命令可以查看基准测试报告。


示例

goos: windows
goarch: amd64
pkg: locks1
cpu: Intel(R) Core(TM) i5-8265U CPU @ 1.60GHz
BenchmarkFib-8           4702662               261.0 ns/op             0 B/op          0 
// 测试函数名-cpu核心数 //执行总次数(b.N的值) 每次执行花费的时间 每次执行申请的内存大小 每次执行申请内存次数
allocs/op
PASS
ok      locks1  1.553s

总结

优先看内存、协程、cpu调优部分 了解常用的Top、调用图、火焰图、源码图等等视图 以及网页端的排查方式

原理讲解

CPU采样

采样对象: 函数调用和他们调用的时间。

采样率:100次/秒,固定值

采样时间:从手动启动到手动结束

采样流程:系统通过设定信号处理函数,并开启计时器,每10ms记录一次CPU信息,每100ms将信息写入到写缓存中,到达指定时间后,关闭计数器,取消信号处理函数。

堆内存采样

采集对象:采集内存分配器在堆上分配和释放的内存。

采样率:每分配512K记录一次

采样时间: 从程序运行开始到采样时

采样指标:分配的内存总大小,分配的总对象数、当前持有的内存大小、当前持有的对象数

协程与线程创建采样

协程采样对象:记录了所有用户发起的,运行中的Goroutine的调用栈信息。

线程采样对象:程序创建的系统线程信息。

采样时间:Stop The World

协程采样流程: Stop The World ——> 遍历allg切片 ———> 输出创建g的堆栈信息 ——> Start The World

线程采样流程: Stop The World ——> 遍历allm链表 ———> 输出创建m的堆栈信息 ——> Start The World

阻塞采样

采样对象:阻塞以及耗时

采样率:阻塞耗时超过指定阈值时记录

采样流程: 阻塞操作 ——> 上报调用栈和消耗时间到分析器 ——> 时间到达阈值后记录采样信息 ——> 最后统计阻塞次数以及耗时

锁采样

采样对象:锁竞争操作的调用栈和耗时

采样率:只记录固定比例的锁操作

采样流程: 锁竞争操作 ——> 上报调用栈和消耗时间到分析器 ——> 是固定比例中的才记录采样信息 ——> 统计锁竞争次数和耗时。

性能优化实践

业务服务优化

基础概念

服务

依赖

调用链路

基础库

基础库优化

Go语言优化