高质量编程与性能调优实战 | 青训营

124 阅读7分钟

一. 简介

首先,Go语言是一种编译型语言,其自身已经非常优化,但是在编写Go代码时,还是有一些优化建议:

1. 避免数据竞争:Go语言的并发模型是基于Goroutines和Channels的,尽量避免使用共享内存,而是通过通信来共享内存。

2. 使用缓冲和非缓冲通道:根据需要选择使用缓冲或非缓冲通道。缓冲通道可以在没有接收者的情况下存储数据,而非缓冲通道则需要接收者准备好才能发送。

3. 避免使用全局变量:全局变量会增加代码的复杂性和可能的数据竞争,尽量避免使用。

4. 使用内置函数:Go语言有很多内置函数,如copy,append等,这些函数经过优化,比手动实现的效率更高。

5. 使用适当的数据结构:选择适当的数据结构可以大大提高代码的效率。例如,如果需要频繁查找,可以使用map而不是slice。

6. 避免不必要的内存分配:例如,尽量使用值传递而不是指针传递,除非传递大的结构体或者需要修改原始数据。

7. 使用sync.Pool来复用对象:如果需要频繁创建和销毁对象,可以使用sync.Pool来复用对象,减少GC的压力。

8. 优化垃圾回收:Go语言的GC是自动的,但是可以通过一些方式来减少GC的压力,例如减少内存分配,使用sync.Pool,避免使用finalizer等。

9. 使用pprof进行性能分析:Go语言自带了pprof工具,可以用来分析程序的CPU使用,内存分配等信息,找出性能瓶颈。

10. 使用适当的并发模型:Go语言支持多种并发模型,如CSP,Actor,Pipeline等,选择适当的模型可以提高代码的效率和可读性。

当然,在对go语言程序进行优化的过程中也需要遵循以下几点:

  1. 程序应当在正确可靠的同时尽量简洁易懂,不增加新的bug。

  2. 程序的优化是相对的,空间和时间的优化未必能同时进行,应当合理的进行分配。

  3. 性能的优化应当根据需求来进行,而不是一味地追求优化本身,越底层的优化逻辑在出现问题时越难修复。

  4. 不过早优化。

二. 实例

1)测试

要想优化我们的程序,首先我们得有一个对程序的评价标准,Go语言的标准库中有一个"testing"包,它包含了一些用于测试和基准测试的函数,下面我们来演示如何使用该工具:

1.创建一个简单函数:

// main.go
package main

func Add(a, b int) int {
    return a + b
}

2.我们按照如下程序来测试它:

//main_test.go
package main

import "testing"

func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Add(4, 5)
    }
}

在这个基准测试中,b.N是一个由测试框架提供的数值,它会根据函数的性能自动调整,以便提供一个稳定的基准。 要运行这个基准测试,你可以在命令行中输入go test -bench=. -benchmen来添加内存显示或者直接运行程序。

goos: windows
goarch: amd64
pkg: test9
cpu: 12th Gen Intel(R) Core(TM) i9-12900H
BenchmarkAdd
BenchmarkAdd-20         1000000000               0.1130 ns/op
PASS

以上为直接运行结果,这个结果告诉我们有关ADD函数运行的相关参数。这就是如何具体操作Go的基准测试。

2)优化建议

对于Slice

1. 预分配Slice的容量:在创建Slice时,如果你知道Slice的大致长度,可以使用make函数预分配足够的容量。这样可以避免在后续的操作中频繁重新分配内存。 以下分别是预分配内存和不分配内存的切片:

package main  
  
func AppendToSliceWithoutCapacity() {  
   var s []int  
   for i := 0; i < 1000; i++ {  
   s = append(s, i)  
   }  
}  

func AppendToSliceWithCapacity() {  
   s := make([]int, 0, 1000)  
   for i := 0; i < 1000; i++ {  
   s = append(s, i)  
   }  
}

由于未设定长度的slice在遇到输入会检测长度,若不足则会先进行扩容操作且会有额外的复制操作,因此会花费更多时间。

2.使用copy函数进行Slice复制:如果你需要复制一个Slice,可以使用copy函数来避免创建新的Slice并逐个复制元素。

若在原有切片的基础上创建新的切片,会导致原有的大切片的内存得不到释放,造成阻塞。

func GetLastBySlice(origin []int) []int {
	return origin[len(origin)-2:]
}

func GetLastByCopy(origin []int) []int {
	result := make([]int, 2)
	copy(result, origin[len(origin)-2:])
	return result
}

可用go test -run=. -v来测试

此外,还有以下建议:

3. 使用append函数一次性添加多个元素:如果你需要向Slice中添加多个元素,可以使用append函数一次性添加,而不是逐个添加。这样可以减少内存分配和拷贝的次数。

4. 使用切片操作避免复制:在对Slice进行操作时,尽量使用切片操作来避免复制整个Slice。例如,使用slice[1:]来获取从索引1到末尾的切片,而不是使用slice[1:len(slice)]。

5. 避免频繁的插入和删除操作:Slice的插入和删除操作会导致元素的移动,这是一个开销较大的操作。如果你需要频繁进行插入和删除操作,可以考虑使用其他数据结构,如链表。

6. 避免在循环中重新分配Slice:如果你在循环中对Slice进行操作,尽量避免在每次迭代中重新分配Slice。可以在循环外部预分配Slice的容量,然后在循环中通过索引来修改元素。

7. 使用sync.Pool来复用Slice:如果你需要频繁地创建和销毁Slice,可以使用sync.Pool来复用Slice,减少内存分配和垃圾回收的开销。

Map的优化也是同理。

string进行操作时使用+和使用strings.builderstrings.buffer的性能也有所不同,其中前者会创建基于两端字符串的新的内存空间,因此占用较大的内存,而后者的内存扩容策略则有更好的性能。

使用atomic包来代替加锁,是基于硬件的操作,具有更好的时间性能,可用于保护一段逻辑而不仅仅是一个变量。

二.性能优化工具pprof

可以参考(github.com/wolfogre/go-pprof-practice )这个网站作者给出的实例

package main  
  
import (  
"log"  
"net/http"  
_ "net/http/pprof"  
"os"  
"runtime"  
"time"  
  
"github.com/wolfogre/go-pprof-practice/animal"  
)  
  
func main() {  
log.SetFlags(log.Lshortfile | log.LstdFlags)  
log.SetOutput(os.Stdout)  
  
runtime.GOMAXPROCS(1)  
runtime.SetMutexProfileFraction(1)  
runtime.SetBlockProfileRate(1)  
  
go func() {  
   if err := http.ListenAndServe(":6060", nil); err != nil {  
      log.Fatal(err)  
    }  
   os.Exit(0)  
}()  
  
for {  
   for _, v := range animal.AllAnimals {  
      v.Live()  
   }  
   time.Sleep(time.Second)  
}  
}

这是一个性能炸弹,在能够正常运行起来后,你可以在浏览器中打开http://localhost:6060/debug/pprof/ 这个页面

f7f677cd8c5446fff5d2a69926db7dc.png

其中包含了这些信息

0cd02c12d3de46ef244db4bcd85df4a.png

但是,在这里可以读取到的信息并没有很好的可读性

我们可以通过使用系统自带的资源管理器查看该炸弹进程的资源占用情况:

6a9c4c0c61199e6466321ab4c84170e.png 可以看到大部分内存已经被其占用,我们也可以使用go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10"来采集10s内的性能情况:

14a4bb68876fa9957d90444eb200ac1.png

在上述代码运行完成后,可以再输入top命令来显示具体程序的占用情况: 45443993154e8a25b4a112ecb122d9f.png

以下是具体参数的意义:

31ca6e490ce79fd03263eb5ec9ba2b7.png

我们发现了tiger.eat占用了最多的资源。

接下来我们还可以使用web命令,在此之前你会需要先安装一个graphviz以下是官网链接(blog.wolfogre.com/redirect/v3… ),它会打开一个页面并展示每个节点之间的调用关系和其资源的占用。

在查找到资源占用过高的节点后我们直接将其注释掉即可,使用list Eat命令可以直接查找其位置

8eaab2602f1dd92545dda6934f93249.png

heap命令:go tool pprof http://localhost:6060/debug/pprof/heap 可以在本地的6060端口打开一个页面来查看堆内存相关的数据,再次使用toplist命令来查询占用内存最高的进程

goroutine,的查看仅需在刚刚的基础上切换后缀即可,在web页面可以通过切换view来获得不同的视图 其中火焰图(flame graph)较为直观也较为常用。

mutex锁和block阻塞也是同理,按照toplistweb的流程。

以上即为对性能优化相关内容的简单介绍。