如何优化一个已有的 Go 程序,提高其性能并减少资源占用 | 青训营

74 阅读6分钟

什么是go语言 ?

Go语言(简称Golang)是一门开源的编程语言,由Google开发。它具有并发性、高效性和简洁性等特点,适用于构建并发和高性能的网络服务、分布式系统和大规模软件。

要优化一个已有的Go程序以提高性能和减少资源占用,我们应该怎么做?

要优化一个已有的Go程序以提高性能和减少资源占用,可以按照以下实践过程和思路进行:

  1. 使用性能分析工具:Go语言内置了性能分析工具pprof(通过net/http/pprof包提供),可以通过它来识别程序的瓶颈和性能问题,并生成可视化的性能报告。使用pprof可以查看CPU和内存使用情况、函数调用图、goroutine分析等。
  2. 采用并发编程:利用Go语言的并发模型可以充分利用多核处理器的性能,并减少阻塞等待的时间。可以使用goroutine和channel来实现并发处理。在评估并发程序时,需要注意避免过度并发,避免竞争条件和锁竞争导致的性能问题。
  3. 减少内存分配:频繁的内存分配和垃圾回收会导致性能下降。可以尝试复用对象、使用对象池、避免不必要的大对象分配等方式来降低内存分配的次数。可以使用Go语言的sync.Pool来实现对象池。
  4. 使用合适的数据结构和算法:选择最适合问题的数据结构和算法可以提高程序的性能。Go语言标准库中提供了丰富的数据结构和算法,例如map、slice、heap等,可以根据实际需求选择适合的数据结构和算法。
  5. 并行处理:对于需要耗时的任务,可以考虑使用并行处理来充分利用多核处理器。Go语言的并发模型非常适合编写并行程序,可以使用goroutine和WaitGroup来实现任务的并行执行。
  6. 使用适当的编译优化选项:Go语言的编译器提供了一些优化选项,例如-gcflags-ldflags等。可以通过适当地设置这些编译选项来提高程序的性能和可执行文件的大小。
  7. 使用缓存和快取:在适当的情况下,可以使用缓存和快取来存储中间结果,避免重复计算。可以使用Go语言的sync.Map或第三方库(如bigcache、gocache等)来实现缓存。
  8. 对关键代码进行手动优化:对于性能瓶颈所在的关键代码,可以进行手动优化。例如,使用更高效的算法、减少循环次数、减少内存访问次数、使用位运算等。
  9. 进行压力测试:在进行优化后,需要进行压力测试来验证优化效果。可以使用工具(如ApacheBench、wrk、vegeta等)对程序进行压力测试,并观察性能指标(如响应时间、吞吐量)的变化。

在优化过程中,需要根据具体情况和需求选择适合的优化方法,并进行综合考虑。同时,在优化时应遵循"量化、定位、优化、测试"的迭代过程,先量化性能问题,然后定位瓶颈,进行优化,最后进行测试验证优化效果。还要注意避免过早的优化和过度优化,确保代码的可维护性和可读性。

eg.现在我们给出一个实践案例 (字符串的拼接)

源代码如下:

package main  
import (  
    "bytes"  
    "fmt"  
    "runtime"  
    "strings"  
)  
func main() {  
    strList := []string{"apple", "banana", "cherry"}  

    // 方法一:使用+运算符进行字符串连接  
    result1 := ""  
    for _, str := range strList {  
    result1 += str  
    }  
    fmt.Println("方法一结果:", result1)  

    // 方法二:使用strings.Join进行字符串连接  
    result2 := strings.Join(strList, "")  
    fmt.Println("方法二结果:", result2)  

    // 方法三:使用bytes.Buffer进行字符串连接  
    var buffer bytes.Buffer  
    for _, str := range strList {  
    buffer.WriteString(str)  
    }  
    result3 := buffer.String()  
    fmt.Println("方法三结果:", result3)  

    var m runtime.MemStats  
    runtime.ReadMemStats(&m)  
    fmt.Printf("Allocated memory: %d bytes\n", m.Alloc)  
    fmt.Printf("Total memory allocated and not yet freed: %d bytes\n", m.Sys)  
   }

运行结果:

image.png

上述代码通过三种不同的方式将字符串列表中的元素进行连接,并输出结果。我们将针对这个代码进行优化。

优化步骤如下:

  1. 目标分析与性能测量:

    • 使用性能分析工具来测量三种字符串连接方法的执行时间和资源占用。
  2. 代码优化:

    • 方法一使用了+运算符,每次都会创建新的字符串对象,导致频繁的内存分配和垃圾回收。我们可以改为使用strings.Builder或bytes.Buffer来减少内存分配的开销。
    • 方法二使用了strings.Join函数,该函数内部也使用了+运算符进行字符串拼接。我们可以尝试使用更高效的方法。
    • 方法三使用了bytes.Buffer,可以继续优化。
  3. 资源管理:

    • 对于方法三,可以在使用完buffer后调用Reset方法来重用buffer,避免重复创建新的buffer对象。

下面是优化后的代码:


package main  
  
import (  
"bytes"  
"fmt"  
"runtime"  
"strings"  
)  
  
func main() {  
    strList := []string{"apple", "banana", "cherry"}  

    // 方法一:使用strings.Builder进行字符串连接  
    var builder1 strings.Builder  
    for _, str := range strList {  
    builder1.WriteString(str)  
    }  
    result1 := builder1.String()  
    fmt.Println("方法一结果:", result1)  

    // 方法二:使用bytes.Buffer进行字符串连接  
    var buffer2 bytes.Buffer  
    for _, str := range strList {  
    buffer2.WriteString(str)  
    }  
    result2 := buffer2.String()  
    fmt.Println("方法二结果:", result2)  

    // 方法三:重用bytes.Buffer进行字符串连接  
    var buffer3 bytes.Buffer  
    for _, str := range strList {  
    buffer3.WriteString(str)  
    }  
    result3 := buffer3.String()  
    buffer3.Reset() // 重用buffer3  
    fmt.Println("方法三结果:", result3)  

    var m runtime.MemStats  
    runtime.ReadMemStats(&m)  
    fmt.Printf("Allocated memory: %d bytes\n", m.Alloc)  
    fmt.Printf("Total memory allocated and not yet freed: %d bytes\n", m.Sys)  
 }

运行结果:

image.png

可以看出,优化还是有成效的

题外话 :减小 Go 代码编译后的二进制体积

对于优化这块我了解的不多,毕竟水平有限仍在学习中 减小编译后的二进制的体积,能够加快程序的发布和安装过程。接下来呢,我们分别从编译选项方面来介绍如何有效地减小 Go 语言编译后的体积。

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

1.基线用例

基线用例为上文中优化前源代码

2.编译选项

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

go build -o server main.go
---

``` text
Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----         8/19/2023   5:53 PM        1931776 server2

根据默认选项编译程序 可以看到 length 为 1931776 byte

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

go build -ldflags="-s -w" -o server main.go
Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----         8/19/2023   5:51 PM        1291776 server
  • -s:忽略符号表和调试信息。
  • -w:忽略DWARFv3调试信息,使用该选项后将无法使用gdb进行调试。 字节数从1931776变为1291776 可以看到体积下降了 33.4%

结尾

本文到此结束 ,如有错误或者遗漏, 欢迎指正