Go高性能编程阅读笔记

Olivia的小跟班 Lv4

题记

​ 本文主要记录Go语言高性能编程 的一书中的笔记(有些笔记也是抄别人的)。

benchmark

  1. 进行性能测试时,尽可能保持测试环境的稳定
  2. 实现 benchmark 测试
    • 位于 _test.go 文件中
    • 函数名以 Benchmark 开头
    • 参数为 b *testing.B
    b.ResetTimer() 可重置定时器
    b.StopTimer() 暂停计时
    b.StartTimer() 开始计时
  3. 执行 benchmark 测试
    go test -bench . 执行当前测试
    b.N 决定用例需要执行的次数
    -bench 可传入正则,匹配用例
    -cpu 可改变 CPU 核数
    -benchtime 可指定执行时间或具体次数
    -count 可设置 benchmark 轮数
    -benchmem 可查看内存分配量和分配次数

pprof

  1. 性能分析类型

    • CPU 性能分析,runtime 每隔 10 ms 中断一次,记录此时正在运行的 goroutines 的堆栈信息
    • 内存性能分析,记录堆内存分配时的堆栈信息,忽略栈内存分配信息,默认每 1000 次采样 1 次
    • 阻塞性能分析,GO 中独有的,记录一个协程等待一个共享资源花费的时间
    • 锁性能分析,记录因为锁竞争导致的等待或延时
  2. CPU 性能分析

    • 使用原生 runtime/pprof 包,通过在 main 函数中添加代码运行可生成性能分析报告:

      1
      2
      pprof.StartCPUProfile(os.Stdout)
      defer pprof.StopCPUProfile()
    • 可通过 go tool pprof -http=:9999 cpu.pprof 在 web 页面查看分析数据

    • 可通过 go tool pprof cpu.prof 交互模式查看分析数据,可使用 help 查看支持的命令和选项

  3. 内存性能分析

    • 使用 pkg/profile 库,通过在 main 函数中添加代码运行可生成性能分析报告:

      1
      defer profile.Start(profile.MemProfile, profile.MemProfileRate(1)).Stop()
    • 同样可通过 web 页面或交互模式查看分析数据

  4. benchmark 生成 profile

  • 可通过在 go test 中添加参数 -cpuprofile=$FILE,-memprofile=$FILE,-blockprofile=$FILE 生成相应的 profile 文件
  • 生成的 profile 文件同样可通过 web 页面或交互模式查看分析数据

字符串拼接

  1. 字符串最高效的拼接方式是结合预分配内存方式 Grow 使用 string.Builder
  2. 当使用 + 拼接字符串时,生成新字符串,需要开辟新的空间
  3. 当使用 strings.Builderbytes.Buffer[]byte 的内存是按倍数申请的,在原基础上不断增加
  4. strings.Builderbytes.Buffer 性能更快,一个重要区别在于 bytes.Buffer 转化为字符串重新申请了一块空间存放生成的字符串变量;而 strings.Builder 直接将底层的 []byte 转换成字符串类型返回

切片性能及陷阱

  1. GO 中的数组变量属于值类型,当数组变量被赋值或传递时,实际上会复制整个数组

  2. 切片本质是数组片段的描述,包括数组的指针,片段的长度和容量,切片操作并不复制切片指向的元素,而是复用原来切片的底层数组

    • 长度是切片实际拥有的元素,使用 len 可得到切片长度

    • 容量是切片预分配的内存能够容纳的元素个数,使用

      1
      cap

      可得到切片容量

      • 当 append 之后的元素小于等于 cap,将会直接利用底层元素剩余的空间
      • 当 append 后的元素大于 cap,将会分配一块更大的区域来容纳新的底层数组,在容量较小的时候,通常是以 2 的倍数扩大
  3. 可能存在只使用了一小段切片,但是底层数组仍被占用,得不到使用,推荐使用 copy 替代默认的 re-slice

for 和 range 的性能比较

  1. range的数据是slice,仅会计算一次,如果在循环中修改切片的长度不会改变本次循环的次数。
  2. 和切片不同的是,迭代过程中,删除还未迭代到的键值对,则该键值对不会被迭代。
  3. range 在迭代过程中返回的是迭代值的拷贝,如果每次迭代的元素的内存占用很低,那么 for 和 range 的性能几乎是一样,例如 []int。但是如果迭代的元素内存占用较高,例如一个包含很多属性的 struct 结构体,那么 for 的性能将显著地高于 range,有时候甚至会有上千倍的性能差异。

Go Reflect 提高反射性能

避免使用反射

Go 空结构体 struct{} 的使用

  1. 空结构体占用空间吗?

    1
    unsafe.Sizeof(struct{}{}) //结果为0
  2. 空结构体的作用

    • 实现集合(Set)
    • 不发送数据的信道(channel)
    • 仅包含方法的结构体

Go struct 内存对齐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type Args struct {
num1 int
num2 int
}

type Flag struct {
num1 int16
num2 int32
}

func main() {
fmt.Println(unsafe.Sizeof(Args{}))
fmt.Println(unsafe.Sizeof(Flag{}))
}

//Args 由 2 个 int 类型的字段构成,在 64位机器上,一个 int 占 8 字节,因此存储一个 Args 实例需要 16 字节。Flag 由一个 int32 和 一个 int16 的字段构成,成员变量占据的字节数为 4+2 = 6,但是 unsafe.Sizeof 返回的结果为 8 字节,多出来的 2 字节是内存对齐的结果。因此,一个结构体实例所占据的空间等于各字段占据空间之和,再加上内存对齐的空间大小。

合理的内存对齐可以提高内存读写的性能,并且便于实现变量操作的原子性。

读写锁和互斥锁的性能比较

  • 读写比为 9:1 时,读写锁的性能约为互斥锁的 8 倍
  • 读写比为 1:9 时,读写锁性能相当
  • 读写比为 5:5 时,读写锁的性能约为互斥锁的 2 倍

如何退出协程 goroutine (超时场景)

如何退出协程 goroutine (超时场景)

如何退出协程 goroutine (其他场景)

如何优雅地关闭 channel

控制协程(goroutine)的并发数量

并发过高导致程序崩溃,如何控制并发数量:

  • 利用 channel 的缓存区(chan struct{})
  • 利用第三方库
  • 调整系统资源的上限
    • ulimit(ulimit -a查看系统当前的设置, ulimit -n ** 调整同时打开的文件句柄数量)
    • 虚拟内存(virtual memory)

Go sync.Pool

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Student struct {
Name string
Age int32
Remark [1024]byte
}

var buf, _ = json.Marshal(Student{Name: "Geektutu", Age: 25})

func main() {
var studentPool = sync.Pool{New: func() interface{} {
return new(Student)
}}
stu := studentPool.Get().(*Student)
json.Unmarshal(buf, stu)
studentPool.Put(stu)
fmt.Println(stu)
}

Go sync.Once

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Once struct {
done uint32
m Mutex
}

func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done) == 0 {
o.doSlow(f)
}
}

func (o *Once) doSlow(f func()) {
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
}

为什么将 done 置为 Once 的第一个字段:done 在热路径中,done 放在第一个字段,能够减少 CPU 指令,也就是说,这样做能够提升性能。

简单解释下这句话:

  1. 热路径(hot path)是程序非常频繁执行的一系列指令,sync.Once 绝大部分场景都会访问 o.done,在热路径上是比较好理解的,如果 hot path 编译后的机器码指令更少,更直接,必然是能够提升性能的。
  2. 为什么放在第一个字段就能够减少指令呢?因为结构体第一个字段的地址和结构体的指针是相同的,如果是第一个字段,直接对结构体的指针解引用即可。如果是其他的字段,除了结构体指针外,还需要计算与第一个值的偏移(calculate offset)。在机器码中,偏移量是随指令传递的附加值,CPU 需要做一次偏移值与指针的加法运算,才能获取要访问的值的地址。因为,访问第一个字段的机器代码更紧凑,速度更快。

Go sync.Cond

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
var done = false

func read(name string, c *sync.Cond) {
c.L.Lock()
for !done {
c.Wait()
}
log.Println(name, "starts reading")
c.L.Unlock()
}

func write(name string, c *sync.Cond) {
log.Println(name, "starts writing")
time.Sleep(time.Second)
c.L.Lock()
done = true
c.L.Unlock()
log.Println(name, "wakes all")
c.Broadcast()
}

func main() {
cond := sync.NewCond(&sync.Mutex{})

go read("reader1", cond)
go read("reader2", cond)
go read("reader3", cond)
write("writer", cond)

time.Sleep(time.Second * 3)
}

减小 Go 代码编译后的二进制体积

1
go build -ldflags="-s -w" -o server main.go && upx -9 server

Go 编译器默认编译出来的程序会带有符号表和调试信息,一般来说 release 版本可以去除调试信息以减小二进制体积。

  • -s:忽略符号表和调试信息。
  • -w:忽略DWARFv3调试信息,使用该选项后将无法使用gdb进行调试。

upx 有很多参数,最重要的则是压缩率,1-91 代表最低压缩率,9 代表最高压缩率。upx 压缩后的程序和压缩前的程序一样,无需解压仍然能够正常地运行,这种压缩方法称之为带壳压缩,压缩包含两个部分:

  • 在程序开头或其他合适的地方插入解压代码;
  • 将程序的其他部分压缩。

执行时,也包含两个部分:

  • 首先执行的是程序开头的插入的解压代码,将原来的程序在内存中解压出来;
  • 再执行解压后的程序。

也就是说,upx 在程序执行时,会有额外的解压动作,不过这个耗时几乎可以忽略。如果对编译后的体积没什么要求的情况下,可以不使用 upx 来压缩。一般在服务器端独立运行的后台服务,无需压缩体积。

Go 逃逸分析

传值会拷贝整个对象,而传指针只会拷贝指针地址,指向的对象是同一个。传指针可以减少值的拷贝,但是会导致内存分配逃逸到堆中,增加垃圾回收(GC)的负担。在对象频繁创建和删除的场景下,传递指针导致的 GC 开销可能会严重影响性能。

一般情况下,对于需要修改原对象值,或占用内存比较大的结构体,选择传指针。对于只读的占用内存较小的结构体,直接传值能够获得更好的性能。

Go 死码消除与调试(debug)模式

死码消除(dead code elimination, DCE)是一种编译器优化技术,用处是在编译阶段去掉对程序运行结果没有任何影响的代码。死码消除有很多好处:减小程序体积,程序运行过程中避免执行无用的指令,缩短运行时间。

  • 使用常量提升性能
  • 可推断的局部变量
  • 调试(debug)模式
  • 条件编译
1
2
3
4
5
6
7
8
9
10
11
12
// +build debug 表示 build tags 中包含 debug 时,该源文件参与编译。
// +build !debug 表示 build tags 中不包含 debug 时,该源文件参与编译。


// +build debug
package main
const debug = true


// +build !debug
package main
const debug = false
  • 标题: Go高性能编程阅读笔记
  • 作者: Olivia的小跟班
  • 创建于 : 2024-11-18 21:13:52
  • 更新于 : 2024-11-19 04:42:47
  • 链接: https://www.youandgentleness.cn/2024/11/18/Go高性能编程阅读笔记/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论