性能调优 | 青训营笔记

84 阅读8分钟

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

高质量编程与性能调优

高质量标准:

  • 正确可靠、简介清晰
  • 边界条件考虑完备
  • 异常情况处理、稳定性保证
  • 易读易维护

注释

公共符号注释

  • 变量、常量、函数、结构
  • 任何函数
  • 不需要注释实现接口的方法

注释使用:

  • 解释代码作用
  • 代码如何做
  • 代码实现原因
  • 代码什么时候会出错

代码格式

使用gofmt自动格式化代码:goLand内置工具

goimports

命名规范

变量

  • 简洁
  • 缩略全大写:如ServeGTTP
  • 全局变量多带上下文信息

函数参数

  • 如时间变量deadline

函数命名

  • 函数名不携带包名的上下文信息,因为包名和函数名总是成对出现
  • 尽量简短
  • 当名为foo的包某个函数返回类型Foo时,开源省略类型信息
  • 当名为foo的包某个函数返回类型T时,可以在函数名中加入类型信息

包命名

  • 只由小写组成
  • 包含上下文信息
  • 不要与标准库同名:sync、strings等
  • 不要用常用变量名作包名:使用bufio而不是buf
  • 使用单数不是复数

控制流程

  • 避免嵌套、保证正常流程:如都包含return去掉多余的else
  • 优先处理错误情况/特殊情况,尽早返回或继续循环来减少嵌套

错误和异常处理

简单错误:仅出现一次的错误

  • 优先使用errors.New来创建匿名变量来直接表述简单错误
  • 如果有格式化的需求,使用fmt.Errorf

复杂错误

  • 错误的Wrap实际上提供一个error嵌套另一个error的能力,从而生成一个error的跟踪链
  • 在fmt.Errorf中使用:%w关键字来将一个错误关联至错误链中
list,_,err := c.GetBytes(cache.Subkey(a.actionID,"srcfiles"))
if err != nil {
    return fmt.Errorf("reading srcfiles list: %w",err)
}

错误判定:

  • 使用errors.Is判断错误为特定错误
  • 在错误链上获取特定类的错误:errors.As
var pathError *fs.PathError
if errors.As(err,&pathError){
    fmt.Println("Failed at path:",pathError.Path)
}else{
    fmt.Println(err)
}

panic

  • 不建议在业务代码中使用panic
  • 调用函数不包含recover会造成程序崩溃
  • 若问题可以被屏蔽或解决,建议使用error代替panic
  • 当程序启动阶段发生不可逆转的错误时,可以在init或main函数中使用panic

recover

  • 只能在被defer的函数中使用
  • 嵌套无法生效
  • 只在当前goroutine生效
  • defer的语句是后进先出
func(s *ss) Token(skipSpace bool,f func(rune) bool)(tok []byte,err error){
    defer func() {
        if e:= recover(); e!=nil{
            if se,ok := e.(scanError); ok{
                err = se.err
            }else {
                panic(e)
            }
        }
    }()
}
  • 在log中记录当前的调用栈
err = fmt.Errorf("gitfs panic: %v\n%s",e,debug.Stack())

性能优化

  • slice预分配内存

    • 尽可能在使用make()初始化切片时提供容量信息
    • 切片本质是数组片段:包括数组指针、片段长度、容量
  • map预分配内存

  • 字符串处理

    • 使用strings.Builder
    • builder.WriteString()
      
    • 字符串在go语言中是不可变类型,占用内存大小是固定的
    • 使用+每次都会重新分配内存
    • 底层是[]byte数组
    • 内存扩容策略,不需要每次拼接重新分配内存

tips:

bytes.Buffer转化为字符串时重新申请了一块空间,strings.Builder直接将底层的[]byte转换成了字符串

  • 空结构体节省内存

    • 实现Set,可以考虑用map代替
  • 使用atomic包

    • func AtomicAddOne(c *atomicCounter){
          atomic.AddInt32(&c,i,1) //实现加锁解锁
      }
      
    • func MutexAddOne(c *mutexCounter){
          c.m.Lock()
          c.i++
          c.m.Unlock()
      }
      
    • 锁的实现是通过操作系统来实现,属于系统调用
    • atomic操作是通过硬件实现,效率比锁高
    • sync.Mutex应该用来保护一段逻辑,不仅仅用于保护一个变量
    • 对于非数值操作,可以使用atomic.Value能承载一个interface{}

性能分析工具

pprof是用于可视化和分析性能分析数据的工具

功能简介

image-20230119165658537.png

实践项目地址:github.com/wolfogre/go…

1.导出数据

网页:在import中加上

"net/http"
_"net/http/pprof"

代码运行地方

go func() {
    log.Println(http.ListAndServe(":6060",nil))
}()

看到nlock和mutex的信息,在代码中加上

runtime.SetBlockProfileRate(1)
runtime.SetMutexProfileFraction(1)

下载信息

方法一:直接在终端运行go tool pprof http://localhost:6060/debug/pprof/XXX

方法二:打开网址:(http://localhost:6060/debug/pprof/XXX)

image-20230119170458985.png

allocs内存分配情况的采样信息可以用浏览器打开,但可读性不高
blocks阻塞操作情况的采样信息可以用浏览器打开,但可读性不高
cmdline显示程序启动命令及参数可以用浏览器打开,这里会显示 ./go-pprof-practice
goroutine当前所有协程的堆栈信息可以用浏览器打开,但可读性不高
heap堆上内存使用情况的采样信息可以用浏览器打开,但可读性不高
mutex锁争用情况的采样信息可以用浏览器打开,但可读性不高
profileCPU 占用情况的采样信息浏览器打开会下载文件
threadcreate系统线程创建情况的采样信息可以用浏览器打开,但可读性不高
trace程序运行跟踪信息浏览器打开会下载文件

分析数据

可以通过任务管理器查看CPU情况,

在交互式终端中使用命令

go tool pprof http://localhost:6060/debug/pprof/profile

输入top命令查看CPU较高的调用:消耗前10的函数

输入list Eat查看问题具体在代码哪里

启动web服务器并自动打开一个网页:

go tool pprof -http=:8000 http://6060/debug/pprof/profile

图形化调用graphviz,需要下载安装!!

采样过程和原理

CPU

  • 操作系统,每10ms向进程发送一次信号
  • 进程:每次就收到信号都会记录调用堆栈
  • 写缓冲:每100ms读取已经记录的调用栈并写入输出流

堆内存

  • 通过内存分配器在堆上分配和释放内存
  • 每分配512K记录一次
  • 采样时间从程序运行开始到采样
  • 采样指标:alloc_space,alloc_objects,inuse_space,inuse_objects
  • 计算方式:inuse = alloc-free

Goroutine和ThreadCreate线程创建

Goroutine

  • 记录所有用户发起且在运行中的goroutine
  • runtime.main的调用栈信息

image-20230119172948276.png

ThreadCreate

  • 记录程序创建的所有系统线程的信息

image-20230119172927530.png

Block阻塞

阻塞操作

  • 采样阻塞操作的次数和耗时
  • 采样率:阻塞耗时超时阈值的才被记录,1为每次阻塞均记录

Mutex锁

锁竞争

  • 采样争抢锁的次数和耗时
  • 采样率:只记录固定比例的锁操作,1为每次加锁均记录

Go语言优化

业务层优化:

  • 针对特定场景、具体问题、具体分析
  • 容易获得较大性能收益

语言运行时优化:

  • 解决更通用的性能问题
  • 考虑更多场景
  • Tradeoffs

数据驱动

  • 自动化性能分析工具-pprof
  • 依靠数据
  • 首先优化最大瓶颈

自动内存管理

动态内存

  • 程序在运行时根据需求动态分配内存:malloc()

  • 自动内存管理(垃圾回收):由程序语言的运行时系统管理动态内存

    • 避免手动管理,专注于实现业务逻辑
    • 保证内存使用的正确性和安全性:double-free、use-after-free

概念

  • Mutator:业务线程,分配新对象
  • Collector:GC线程,找到存活对象,回收死亡的内存空间
  • Serial GC:只有一个collector
  • Parallel GC:支持多个collectors同时回收GC
  • Concurrent GC:mutator和collector可以同时执行

追踪垃圾回收

  • 对象被回收的条件:指针指向关系不可达的对象

  • 标记根对象:静态变量、全局变量、常量、线程栈等

  • 标记:找到可达对象:求指针指向关系的传递闭包:从根对象出发找到所有可达对象

  • 清理:所有不可达对象

    • 将存活对象复制到另外的内存空间(Copying GC)
    • 将死亡对象的内存标记为“可分配”(Mark-sweep GC)
    • 移动并整理存活对象(Mark-compact GC)

分代GC

  • 年轻代

    • 常规的对象分配
    • 存活对象很少,用copying collection
    • GC吞吐率高
  • 老年代

    • 对象趋向于一直存活,反复复制开销大
    • 可以采用mark-sweep collection

引用计数

  • 每个对象都有一个与之关联的引用数目
  • 对象存活的条件:当且仅当引用数大于0

优点:

  • 内存管理的操作分担到程序执行过程中
  • 不需要了解runtime的实现细节

缺点:

  • 维护开销大:原子操作保证对引用技术操作的原子性和可见性
  • 无法回收环形数据结构 weak reference
  • 内存开销:每个对象都引入额外的内存空间存储引用数目
  • 回收内存依然可能引发暂停

内存分配

  • 目标:对象在heap上分配内存
  • 提前内存分块
  • 对象分配:根据对象的大小选择最合适的块返回

缓存

  • TCMalloc:thread caching
  • 每个p包含一个mcache用于快速分配,为绑定p上的g分配对象
  • mcache管理一组mspan
  • 当mcache中的mspan分配完毕,向mcentral申请带有未分配块的mspan
  • 当mspan中没有分配对象,mspan会被缓存在mcentral中,不是立刻释放归还给OS

image-20230119210822573.png

编译器优化

结构

系统软件

  • 识别符合语法和非法的程序
  • 生成正确且高效的代码

分析部分

  • 词法分析
  • 语法分析
  • 语义分析
  • 中间代码生成

综合部分

  • 代码优化
  • 代码生成

静态分析

不执行程序代码,推导程序的行为,分析程序的性质

控制流:程序执行的流程

数据流:数据在控制流上的传递