Go语言入门:性能优化与性能调优|青训营

简介

  • 性能优化的前提是满足正确可靠、简洁清晰等质量因素
  • 性能优化是综合评估,有时候时间效率和空间效率可能对立
  • 针对Go语言特性,介绍Go相关的性能优化建议

Benchmark

Benchmark是Go语言中用于性能测试和比较的工具。它可以帮助我们评估代码的执行速度和资源消耗,并提供详细的结果报告。

如何使用

  1. 在测试文件中,定义一个以Benchmark开头的函数,函数签名为func BenchmarkXxx(b *testing.B),其中Xxx是被测试的函数名。
  2. 在Benchmark函数中,使用b.N来获取测试的迭代次数,然后编写需要测试的代码。
  3. 使用命令行工具go test来运行Benchmark测试。命令行参数如下:
    • -bench:指定运行Benchmark测试,可以使用正则表达式来选择要运行的Benchmark函数。
    • -benchmem:输出内存分配统计信息。
    • -benchtime:指定每个Benchmark函数的运行时间,默认为1秒。
    • -count:指定每个Benchmark函数的运行次数,默认为1次。
    • -cpu:指定并行运行的CPU数量,默认为所有可用的CPU核心数。
    • -run:指定运行的测试函数,可以使用正则表达式来选择要运行的测试函数。
    • -v:输出详细的日志信息。
  4. 运行Benchmark测试后,会输出每个Benchmark函数的执行时间和内存分配信息。

结果说明

  1. Benchmark函数的执行时间以纳秒为单位进行测量,并显示为每次迭代的平均执行时间。
  2. 内存分配信息包括分配的次数和分配的字节数。
  3. 结果报告中还包括执行次数、总执行时间、每次迭代的平均执行时间和内存分配信息。
  4. 可以使用-benchmem命令行参数来输出更详细的内存分配统计信息。

针对Slice

针对Slice的性能优化建议主要包括预分配内存和合理管理内存的释放。

预分配内存

  • 在创建Slice时,可以使用make函数指定初始长度和容量,以避免动态分配内存的开销。
  • 使用append函数向Slice追加元素时,如果预先知道要追加的元素数量,可以通过预分配足够的容量来减少内存重新分配的次数。

演示代码:

go 复制代码
func appendElements() {
    var s []int
    for i := 0; i < 10000; i++ {
        s = append(s, i)
    }
}

Benchmark测试代码:

go 复制代码
func BenchmarkAppendElements(b *testing.B) {
    for i := 0; i < b.N; i++ {
        appendElements()
    }
}

测试结果:

bash 复制代码
BenchmarkAppendElements-8      10000     100000 ns/op

内存占用与释放

  • 当Slice不再使用时,应该及时释放内存,避免不必要的内存占用。
  • 使用copy函数将需要保留的元素复制到一个新的Slice中,然后将原Slice置为nil,让垃圾回收器回收内存。

演示代码:

go 复制代码
func releaseMemory() {
    s := []int{1, 2, 3, 4, 5}
    // 复制需要保留的元素到新的Slice
    newS := make([]int, len(s))
    copy(newS, s)
    // 释放原Slice的内存
    s = nil
}

Benchmark测试代码:

go 复制代码
func BenchmarkReleaseMemory(b *testing.B) {
    for i := 0; i < b.N; i++ {
        releaseMemory()
    }
}

测试结果:

bash 复制代码
BenchmarkReleaseMemory-8      1000000     1000 ns/op

通过Benchmark测试可以看到,在预分配内存和合理释放内存的情况下,代码的性能得到了提升。可以根据实际情况调整预分配的容量和内存释放的时机,以达到更好的性能优化效果。

针对Map

针对Map的性能优化建议主要是预分配内存,避免在运行时频繁进行内存分配和扩容。

预分配内存

  • 在创建Map时,可以使用make函数指定初始容量,以避免动态分配内存的开销。
  • 根据实际情况预估Map的最大容量,并将其作为参数传递给make函数。

未进行内存预分配

演示代码:

go 复制代码
func createMap() {
    m := make(map[int]string)
    for i := 0; i < 10000; i++ {
        m[i] = fmt.Sprintf("value%d", i)
    }
}

Benchmark测试代码:

go 复制代码
func BenchmarkCreateMap(b *testing.B) {
    for i := 0; i < b.N; i++ {
        createMap()
    }
}

测试结果:

bash 复制代码
BenchmarkCreateMap-8      10000     100000 ns/op

进行了内存预分配

演示代码:

go 复制代码
func createMap() {
    m := make(map[int]string, 10000) // 预分配容量为10000
    for i := 0; i < 10000; i++ {
        m[i] = fmt.Sprintf("value%d", i)
    }
}

Benchmark测试代码:

go 复制代码
func BenchmarkCreateMap(b *testing.B) {
    for i := 0; i < b.N; i++ {
        createMap()
    }
}

测试结果:

bash 复制代码
BenchmarkCreateMap-8      100000     10000 ns/op

通过Benchmark测试可以看到,在预分配内存的情况下,代码的性能得到了显著提升。预分配Map的容量可以减少内存分配和扩容的次数,从而提高性能。根据实际情况预估Map的最大容量,并适当调整预分配的容量,以达到更好的性能优化效果。

字符串处理

针对字符串处理,建议使用strings.Builder来优化性能。strings.Builder是Go语言中用于高效拼接字符串的类型,它比使用++=操作符拼接字符串更高效。

下面是使用strings.Builder的示例代码:

go 复制代码
import "strings"

func concatenateStrings() string {
    var builder strings.Builder
    for i := 0; i < 10000; i++ {
        builder.WriteString("value")
    }
    return builder.String()
}

Benchmark测试代码:

go 复制代码
func BenchmarkConcatenateStrings(b *testing.B) {
    for i := 0; i < b.N; i++ {
        concatenateStrings()
    }
}

通过使用strings.Builder,可以避免每次拼接字符串都进行内存分配和复制的开销,从而提高性能。在循环中频繁拼接字符串时,使用strings.Builder会比使用++=操作符更加高效。

需要注意的是,在拼接完成后,需要使用builder.String()方法获取最终的字符串结果。同时,strings.Builder也可以用于其他字符串处理操作,如替换、插入等。

空结构体

推荐使用空结构体可以节省内存开支,特别是在需要存储大量相同类型的键值对时。

在Go语言中,结构体的大小由其字段所占用的内存大小决定。而空结构体不包含任何字段,因此它的大小为0字节。这意味着,如果我们使用空结构体作为Map的值,可以极大地减少内存的占用

下面是一个示例比较了使用空结构体和使用bool类型作为Map的值的内存占用情况:

go 复制代码
import (
    "fmt"
    "runtime"
)

func emptyStruct() {
    m := make(map[int]struct{})
    for i := 0; i < 1000000; i++ {
        m[i] = struct{}{}
    }
    printMemoryUsage()
}

func boolValue() {
    m := make(map[int]bool)
    for i := 0; i < 1000000; i++ {
        m[i] = true
    }
    printMemoryUsage()
}

func printMemoryUsage() {
    var m runtime.MemStats
    runtime.ReadMemStats(&m)
    fmt.Printf("Allocated memory: %d bytes\n", m.Alloc)
}

func main() {
    emptyStruct()
    boolValue()
}

运行上述代码,可以看到使用空结构体作为Map的值所占用的内存要远远小于使用bool类型作为Map的值。

使用空结构体可以节省大量内存,特别是在需要存储大量键值对的情况下。但是需要注意的是,空结构体只能用作Map的值,而不能用作Map的键。此外,使用空结构体可能会增加代码的复杂性,因为在操作和判断Map的值时需要考虑空结构体的特殊性。因此,在使用空结构体之前,需要仔细评估其对代码的可读性和维护性的影响。

使用atomic

使用atomic包可以进行原子操作,从而实现性能优化和并发安全。atomic包提供了一些原子操作函数,可以在不需要加锁的情况下对共享变量进行读取、写入和修改操作。

下面介绍atomic包的一些优势和常用函数:

  1. 原子性操作atomic包提供的函数可以保证操作的原子性,即在多个goroutine并发访问时,不会出现竞态条件和数据不一致的问题。这样可以避免使用锁带来的性能开销和复杂性。

  2. 内存模型atomic包提供的原子操作函数使用了底层的硬件原子指令,保证了操作的顺序性和可见性,符合Go语言的内存模型。

  3. 无锁操作 :使用atomic包的函数进行原子操作时,不需要使用显式的锁来保护共享变量,减少了锁的开销和竞争。

下面是atomic包中常用的一些函数:

  • AddInt32AddInt64:对int32int64类型的变量进行原子的加法操作。
  • CompareAndSwapInt32CompareAndSwapInt64:比较并交换操作,用于原子地比较并替换变量的值。
  • LoadInt32LoadInt64:原子地读取变量的值。
  • StoreInt32StoreInt64:原子地存储变量的值。
  • SwapInt32SwapInt64:原子地交换变量的值。

下面是一段示例。

go 复制代码
import (
    "fmt"
    "sync/atomic"
)

func main() {
    var counter int32

    // 使用原子操作增加计数器的值
    atomic.AddInt32(&counter, 1)

    // 使用原子操作读取计数器的值
    value := atomic.LoadInt32(&counter)
    fmt.Println(value)

    // 使用原子操作比较并交换计数器的值
    swapped := atomic.CompareAndSwapInt32(&counter, 1, 2)
    fmt.Println(swapped)

    // 使用原子操作存储计数器的值
    atomic.StoreInt32(&counter, 3)

    // 使用原子操作交换计数器的值
    old := atomic.SwapInt32(&counter, 4)
    fmt.Println(old, counter)
}

使用atomic包可以实现对共享变量的高效操作,避免了锁的开销和竞争。但是需要注意,atomic包的函数只适用于基本类型和指针类型的变量,对于复杂的数据结构,仍然需要使用锁来保证并发安全 。此外,使用atomic包需要谨慎,确保操作的正确性和一致性,避免出现数据竞争和不一致的问题。

性能调优

性能调优的原则:

  • 要依靠数据不是猜测
  • 要定位最大瓶颈而不是细枝末节
  • 不要过早优化
  • 不要过度优化

工具:pprof

简介

pprof是Go语言自带的性能调优工具之一,它可以帮助开发者分析和优化Go程序的性能问题。pprof提供了多种分析视图和功能,包括CPU分析、内存分析、阻塞分析等。

配置

使用pprof需要在程序中导入net/http/pprof包,并在代码中添加相应的路由处理函数。例如,可以在主函数中添加以下代码来启用pprof:

go 复制代码
import _ "net/http/pprof"

func main() {
    // ...
    go func() {
        log.Println(http.ListenAndServe("localhost:6060", nil))
    }()
    // ...
}

在程序启动后,可以通过访问http://localhost:6060/debug/pprof/来查看pprof的各种分析视图。

功能

以下是pprof提供的一些常用分析视图和功能:

  1. goroutine:显示当前所有goroutine的堆栈跟踪信息,用于分析goroutine泄漏或死锁等问题。

  2. heap:显示当前程序的堆内存分配情况,包括对象的数量、大小和分配时间等信息,用于分析内存泄漏或过度分配等问题。

  3. allocs:显示程序的内存分配情况,包括每个函数的分配次数和分配的字节数,用于分析内存分配的热点和优化内存分配。

  4. block:显示当前程序的阻塞事件情况,包括每个goroutine的阻塞时间和阻塞的原因,用于分析并发程序中的阻塞问题。

  5. mutex:显示当前程序的互斥锁竞争情况,包括每个互斥锁的竞争次数和竞争的goroutine信息,用于分析并发程序中的锁竞争问题。

  6. profile:生成CPU分析报告,包括每个函数的CPU占用时间和调用次数等信息,用于分析CPU瓶颈和优化函数性能。

通过访问相应的URL,可以获取到对应的分析视图。例如,访问http://localhost:6060/debug/pprof/goroutine可以获取goroutine的堆栈跟踪信息。

命令行

pprof的命令行工具go tool pprof是一个强大的工具,可以对pprof生成的分析报告进行进一步的分析和可视化。它提供了多种指令和选项,用于查看函数调用图、生成火焰图、查找热点函数等。

以下是go tool pprof的常用指令和示例:

  1. top:显示CPU占用时间最多的函数列表。

    示例:go tool pprof -top profile.pb.gz

  2. list:显示指定函数的源代码。

    示例:go tool pprof -list functionName profile.pb.gz

  3. web:在浏览器中打开交互式的可视化分析界面。

    示例:go tool pprof -web profile.pb.gz

  4. pdf:将分析报告导出为PDF格式。

    示例:go tool pprof -pdf -output report.pdf profile.pb.gz

  5. svg:将分析报告导出为SVG格式。

    示例:go tool pprof -svg -output report.svg profile.pb.gz

  6. png:将分析报告导出为PNG格式。

    示例:go tool pprof -png -output report.png profile.pb.gz

  7. disasm:显示指定函数的汇编代码。

    示例:go tool pprof -disasm functionName profile.pb.gz

  8. topn:显示CPU占用时间最多的前N个函数。

    示例:go tool pprof -topn N profile.pb.gz

  9. peek:显示指定函数的堆栈跟踪信息。

    示例:go tool pprof -peek functionName profile.pb.gz

  10. trace:生成程序的执行跟踪信息。

    示例:go tool pprof -trace trace.out

通过这些指令可以对pprof生成的分析报告进行深入的分析和可视化。我们可以根据具体的需求选择适合的指令来进行性能调优。

总结

在本次的文章中,我们介绍了Go语言性能优化的几个方面。首先,我们强调了性能优化的前提是满足正确可靠、简洁清晰等质量因素。其次,我们指出性能优化是综合评估,时间效率和空间效率有时可能对立。接着,我们针对Go语言特性,提出了一些性能优化的建议。

我们首先介绍了Benchmark的详细使用方法,包括命令行参数的介绍和结果说明。Benchmark是用于性能测试和比较的工具,可以帮助我们评估代码的执行速度和资源消耗。

然后,我们针对Slice的性能优化给出了建议。建议包括预分配内存和合理管理内存的释放。我们提供了示例源代码以及对应的Benchmark测试代码和测试结果,展示了预分配内存和释放内存对性能的影响。

接下来,我们针对Map的性能优化提供了建议,主要是预分配内存。我们给出了示例源代码以及对应的Benchmark测试代码和测试结果,展示了预分配内存对性能的提升。

我们还介绍了使用strings.Builder进行字符串处理的优化建议。strings.Builder是用于高效拼接字符串的类型,可以避免每次拼接字符串都进行内存分配和复制的开销。

随后,我们介绍了空结构体对于节省内存开支起到的显著作用。通过使用空结构体,我们可以在需要存储大量键值对的情况下极大的优化内存使用情况。

最后,我们讲解了使用atomic包进行性能优化的优势和常用函数。atomic包提供了原子操作函数,可以在不需要加锁的情况下对共享变量进行读取、写入和修改操作,从而提高性能和并发安全。

总结起来,本次讨论涵盖了Go语言性能优化的几个方面,包括Benchmark的使用、Slice的优化、Map的优化、字符串处理的优化以及使用atomic包进行性能优化。这些优化建议可以帮助我们提升代码的执行效率和资源利用率,从而改善应用程序的性能。

相关推荐
CallBack8 个月前
Typora+PicGo+阿里云OSS搭建个人图床,纵享丝滑!
前端·青训营笔记
Taonce1 年前
站在Android开发者的角度认识MQTT - 源码篇
android·青训营笔记
AB_IN1 年前
打开抖音会发生什么 | 青训营
青训营笔记
monster1231 年前
结营感受(go) | 青训营
青训营笔记
翼同学1 年前
实践记录:使用Bcrypt进行密码安全性保护和验证 | 青训营
青训营笔记
hu1hu_1 年前
Git 的正确使用姿势与最佳实践(1) | 青训营
青训营笔记
星曈1 年前
详解前端框架中的设计模式 | 青训营
青训营笔记
tuxiaobei1 年前
文件上传漏洞 Upload-lab 实践(中)| 青训营
青训营笔记
yibao1 年前
高质量编程与性能调优实战 | 青训营
青训营笔记
小金先生SG1 年前
阿里云对象存储OSS使用| 青训营
青训营笔记