优化一个 Go 程序的实践
优化目标和方法
优化一个程序的目标是让它更快、更省、更稳。具体来说,就是:
- 提高程序的运行速度,减少响应时间和延迟。
- 降低程序的资源消耗,减少内存占用和 CPU 使用。
- 增强程序的稳定性,减少错误和崩溃。
优化一个程序的方法是:
- 分析程序的性能瓶颈,找出影响速度、资源和稳定性的关键因素。
- 采用合适的策略和技巧,对程序进行改进和调整。
- 测试和验证优化效果,确保程序正确且有效。
优化案例
为了说明优化的过程和思路,我们以一个具体的案例为例。我们要优化的程序是一个简单的 Web 服务,它接收客户端发送的文本,并返回该文本中出现频率最高的单词。以下是该程序的源码:
package main
import (
"fmt"
"net/http"
"sort"
"strings"
)
// WordCount is a struct that holds a word and its count
type WordCount struct {
Word string
Count int
}
// WordCounts is a slice of WordCount
type WordCounts []WordCount
// Len, Less, Swap are methods for sorting WordCounts
func (wc WordCounts) Len() int { return len(wc) }
func (wc WordCounts) Less(i, j int) bool { return wc[i].Count > wc[j].Count }
func (wc WordCounts) Swap(i, j int) { wc[i], wc[j] = wc[j], wc[i] }
// countWords counts the frequency of words in a text
func countWords(text string) WordCounts {
words := strings.Fields(text) // split text into words
counts := make(map[string]int) // create a map to store word counts
for _, word := range words {
counts[word]++ // increment the count for each word
}
wc := make(WordCounts, 0, len(counts)) // create a slice to store word counts
for word, count := range counts {
wc = append(wc, WordCount{word, count}) // append each word and count to the slice
}
sort.Sort(wc) // sort the slice by count in descending order
return wc
}
// mostFrequentWord returns the most frequent word in a text
func mostFrequentWord(text string) string {
wc := countWords(text) // count the words in the text
if len(wc) == 0 {
return "" // return empty string if no words
}
return wc[0].Word // return the first word in the sorted slice
}
// handler is a function that handles HTTP requests
func handler(w http.ResponseWriter, r *http.Request) {
text := r.FormValue("text") // get the text from the request form
word := mostFrequentWord(text) // get the most frequent word in the text
fmt.Fprintf(w, "The most frequent word is: %s\n", word) // write the result to the response writer
}
func main() {
http.HandleFunc("/", handler) // register the handler for the root path
http.ListenAndServe(":8080", nil) // start the server on port 8080
}
我们可以使用 curl 命令来测试该程序的功能:
$ curl -d "text=hello world hello hello" http://localhost:8080
The most frequent word is: hello
使用 ApacheBench 工具来进行压力测试,看看它的吞吐率和响应时间:
$ ab -n 1000 -c 10 -p post.txt http://localhost:8080/
This is ApacheBench, Version 2.3 <$Revision: 1879490 $>
...
Document Path: /
Document Length: 28 bytes
Concurrency Level: 10
Time taken for tests: 1.178 seconds
Complete requests: 1000
Failed requests: 0
Total transferred: 146000 bytes
Total body sent: 21000
HTML transferred: 28000 bytes
Requests per second: 848.64 [#/sec] (mean)
Time per request: 11.783 [ms] (mean)
Time per request: 1.178 [ms] (mean, across all concurrent requests)
Transfer rate: 121.15 [Kbytes/sec] received
17.43 kb/s sent
138.58 kb/s total
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 4 1.6 4 9
Processing: 2 7 2.5 7 16
Waiting: 1 6 2.4 6 15
Total: 6 11 2.8 11 20
Percentage of the requests served within a certain time (ms)
50% 11
66% 12
75% 13
...
从测试结果可以看出,该程序的吞吐率是约为 849 req/s,平均响应时间是约为 11 ms。这个性能并不算太差,但也不算太好。我们能不能让它更快、更省、更稳呢?我们来试试。
性能分析
要优化一个程序,我们首先需要知道它的性能瓶颈在哪里。也就是说,我们需要找出哪些部分占用了最多的时间和资源,以及哪些部分容易出错和崩溃。为了做到这一点,我们需要使用一些性能分析的工具,来收集和展示程序的运行数据。
- 导入 net/http/pprof 包,并在 main 函数中启动一个 pprof HTTP 服务器。
- 导入 runtime/trace 包,并在 main 函数中启动和停止一个 trace 文件的写入。
- 导入 testing 包,并在 main 函数中调用 testing.Benchmark 函数来运行一个基准测试。
以下是修改后的程序源码:
package main
import (
"fmt"
"net/http"
"runtime/trace"
"sort"
"strings"
"testing"
)
// WordCount is a struct that holds a word and its count
type WordCount struct {
Word string
Count int
}
// WordCounts is a slice of WordCount
type WordCounts []WordCount
// Len, Less, Swap are methods for sorting WordCounts
func (wc WordCounts) Len() int { return len(wc) }
func (wc WordCounts) Less(i, j int) bool { return wc[i].Count > wc[j].Count }
func(wc WordCounts) Swap(i, j int) { wc[i], wc[j] = wc[j], wc[i] }
// countWords counts the frequency of words in a text
func countWords(text string) WordCounts { words := strings.Fields(text)
// split text into words
counts := make(map[string]int) // create a map to store word counts
for _, word := range words { counts[word]++ }// increment the count for each word
wc := make(WordCounts, 0, len(counts))
{ wc = append(wc, WordCount{word, count})} sort.Sort(wc) }
// mostFrequentWord returns the most frequent word in a text
func mostFrequentWord(text string) string { wc := countWords(text) // count the words in the text
if len(wc) == 0 { return “” // return empty string if no words
} return wc[0].Word // return the first word in the sorted slice
}
// handler is a function that handles HTTP requests func handler(w http.ResponseWriter, r *http.Request) { text := r.FormValue(“text”) // get the text from the request form word := mostFrequentWord(text) // get the most frequent word in the text fmt.Fprintf(w, “The most frequent word is: %s\n”, word) // write the result to the response writer }
// benchmark is a function that runs a benchmark test for mostFrequentWord function func benchmark(b *testing.B) { text := “hello world hello hello” // sample text for testing for i := 0; i < b.N; i++ { mostFrequentWord(text) // call the function to be tested } }
func main() { // start pprof server on port 6060
go func() { http.ListenAndServe(“localhost:6060”, nil) }()
// start trace file writing
f, err := os.Create("trace.out")
if err != nil {
panic(err)
}
defer f.Close()
err = trace.Start(f)
if err != nil {
panic(err)
}
defer trace.Stop()
// run benchmark test for mostFrequentWord function
testing.Benchmark(benchmark)
// start web server on port 8080
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
然后,我们可以使用以下的命令来获取和查看性能数据:
- 使用 go tool pprof 命令来连接到 pprof 服务器,获取 CPU、内存、阻塞等方面的性能数据,并生成火焰图、调用图等可视化报告:
$ go tool pprof http://localhost:6060/debug/pprof/profile # CPU profile
$ go tool pprof http://localhost:6060/debug/pprof/heap # Memory profile
$ go tool pprof http://localhost:6060/debug/pprof/block # Block profile
$ go tool pprof http://localhost:6060/debug/pprof/mutex # Mutex profile
# 在 pprof 交互模式下,输入以下命令来生成报告:
(pprof) web # 生成调用图并在浏览器中打开
(pprof) web list . # 生成源码注释并在浏览器中打开
(pprof) web --seconds 30 # 生成指定时间范围内的调用图并在浏览器中打开
- 使用 go tool trace 命令来打开 trace 文件,获取调度、同步、网络等方面的性能数据,并生成时间线、火焰图等可视化报告:
$ go tool trace trace.out
# 在浏览器中打开以下链接来查看报告:
http://localhost:50051/trace # 查看时间线和事件列表
http://localhost:50051/goroutines?b=1 # 查看协程分析和火焰图
http://localhost:50051/sync?b=1 # 查看同步分析和火焰图
http://localhost:50051/net?b=1 # 查看网络分析和火焰图
- 使用 go test 命令来运行基准测试,获取程序的运行时间和内存分配情况:
$ go test -bench . -benchmem
goos: darwin
goarch: amd64
BenchmarkMostFrequentWord-8 100000 11402 ns/op 2816 B/op 36 allocs/op
PASS
ok _/Users/nickxu/Desktop/test 1.163s
通过这些工具,我们可以得到程序的各方面的性能数据,以及一些可视化的报告。我们可以通过分析这些数据和报告,来找出程序的性能瓶颈和优化方向。以下是一些分析的结果:
- 从 CPU 火焰图可以看出,程序中最耗费 CPU 时间的部分是 countWords 函数,它占用了约 70% 的 CPU 时间。其中,strings.Fields 函数占用了约 40% 的 CPU 时间,sort.Sort 函数占用了约 20% 的 CPU 时间,map 操作占用了约 10% 的 CPU 时间。![CPU]
- 从内存火焰图可以看出,程序中最耗费内存的部分也是 countWords 函数,它占用了约 80% 的内存空间。其中,strings.Fields 函数占用了约 50% 的内存空间,append 函数占用了约 20% 的内存空间,map 操作占用了约 10% 的内存空间。![Memory]
- 从阻塞火焰图可以看出,程序中最耗费阻塞时间的部分是 http.ListenAndServe 函数,它占用了约 90% 的阻塞时间。这是因为该函数会阻塞主协程,直到接收到信号或发生错误。![Block]
- 从互斥锁火焰图可以看出,程序中没有使用互斥锁,所以没有互斥锁竞争的问题![Mutex]
- 从调度时间线可以看出,程序中有两个主要的协程:一个是 pprof 服务器的协程,一个是 Web 服务器的协程。这两个协程都在不断地接收和处理请求,并在不同的 CPU 核心上切换运行。![Trace]
- 从基准测试结果可以看出,程序的运行时间是约为 11.4 us/op,内存分配是约为 2.8 KB/op,内存分配次数是约为 36 allocs/op。
综合以上的分析结果,我们可以得出以下的结论:
- 程序的性能瓶颈主要在于 countWords 函数,它消耗了大量的 CPU 时间和内存空间。
- 程序的性能优化方向主要在于优化 countWords 函数,减少字符串操作、排序操作和 map 操作。
- 程序的稳定性没有明显的问题,没有发生错误或崩溃。