可觀察性與性能在 go 中的實踐
先測量
“過早的優化是萬惡之源”——Donald Knuth
Go 有兩個在性能調優方面非常寶貴的工具:一個分析器和一個基準測試工具。探查器幫助找到問題點,基準顯示優化的結果。有關這些工具的介紹,請參閱 Dave Cheney 的 How to write benchmarks in Go 和 Russ Cox 的 Profiling Go Programs 。下面是我在基準測試和分析器中發現的幾種具體技術。基準測試的源代碼在 Github 上。
重用內存
每次內存分配都有幾個潛在的成本。Go 運行時必須確保內存被初始化爲零值。垃圾收集器必須跟蹤對值的引用並最終清理它。額外的內存使用也降低了 CPU 緩存命中的可能性。
這個簡單的例子用 1 填充一個最多 1024 字節的切片。
func BenchmarkNewBuffers(b *testing.B) {
for i := 0; i < b.N; i++ {
n := rand.Intn(1024)
buf := make([]byte, n)
// Do something with buffer
for j := 0; j < n; j++ {
buf[j] = 1
}
}
}
func BenchmarkReuseBuffers(b *testing.B) {
sharedBuf := make([]byte, 1024)
for i := 0; i < b.N; i++ {
n := rand.Intn(1024)
buf := sharedBuf[0:n]
// Do something with buffer
for j := 0; j < n; j++ {
buf[j] = 1
}
}
}
請注意用於測量內存分配的 -test.benchmem 標誌。
jack@hk-47~/dev/go/src/github.com/jackc/go_pgx_perf_observations$ go test -test.bench=Buffers -test.benchmem
testing: warning: no tests to run
PASS
BenchmarkNewBuffers 2000000 1033 ns/op 540 B/op 0 allocs/op
BenchmarkReuseBuffers 5000000 436 ns/op 0 B/op 0 allocs/op
ok github.com/jackc/go_pgx_perf_observations 5.704s
每次迭代分配一個新的緩衝區要慢得多。顯然,在緩衝區上完成的工作越多,消除分配的相對影響就越小。令人驚訝的是,兩個版本都顯示 0 allocs/op。怎麼可能?讓我們使用 -gcflags=-m 選項重新運行測試,讓 Go 告訴我們詳細信息。
jack@hk-47~/dev/go/src/github.com/jackc/go_pgx_perf_observations$ go test -gcflags=-m -test.bench=Buffers -test.benchmem
# github.com/jackc/go_pgx_perf_observations_test
<snip/>
./bench_test.go:15: BenchmarkNewBuffers b does not escape
./bench_test.go:18: BenchmarkNewBuffers make([]byte, n) does not escape
./bench_test.go:27: BenchmarkReuseBuffers b does not escape
./bench_test.go:28: BenchmarkReuseBuffers make([]byte, 1024) does not escape
<snip/>
Go 編譯器執行逃逸分析。如果分配沒有逃脫函數,它可以存儲在堆棧中並完全避免垃圾收集器。
因此,在許多分配確實逃逸到堆的現實世界系統中,減少分配可能會產生更大的影響。
緩衝輸入輸出
Go 默認不緩衝 IO。bufio 包提供緩衝 IO 。這可以在性能上產生巨大的差異。
func BenchmarkUnbufferedFileWrite(b *testing.B) {
file, err := os.Create("unbuffered.test")
if err != nil {
b.Fatalf("Unable to create file: %v", err)
}
defer func() {
file.Close()
os.Remove(file.Name())
}()
for i := 0; i < b.N; i++ {
fmt.Fprintln(file, "Hello world")
}
}
func BenchmarkBufferedFileWrite(b *testing.B) {
file, err := os.Create("buffered.test")
if err != nil {
b.Fatalf("Unable to create file: %v", err)
}
defer func() {
file.Close()
os.Remove(file.Name())
}()
writer := bufio.NewWriter(file)
defer writer.Flush()
for i := 0; i < b.N; i++ {
fmt.Fprintln(writer, "Hello world")
}
}
jack@hk-47~/dev/go/src/github.com/jackc/go_pgx_perf_observations$ go test -test.bench=Write
testing: warning: no tests to run
PASS
BenchmarkUnbufferedFileWrite 1000000 2588 ns/op
BenchmarkBufferedFileWrite 10000000 271 ns/op
ok github.com/jackc/go_pgx_perf_observations 5.626s
將 “Hello, world” 重複寫入文件的簡單測試表明,使用緩衝寫入器可將性能提高 9 倍以上。
二進制與文本格式
PostgreSQL 允許以二進制或文本格式傳輸數據。二進制格式的性能遠比文本格式快。這是因爲通常唯一需要的處理是從網絡字節順序轉換。二進制格式對於 PostgreSQL 服務器也應該更高效,它可能是一種更緊湊的傳輸格式。但是,我們會將我們的基準隔離到 int32 和 time.Time 值的解析。
func BenchmarkParseInt32Text(b *testing.B) {
s := "12345678"
expected := int32(12345678)
for i := 0; i < b.N; i++ {
n, err := strconv.ParseInt(s, 10, 32)
if err != nil {
b.Fatalf("strconv.ParseInt failed: %v", err)
}
if int32(n) != expected {
b.Fatalf("strconv.ParseInt decoded %v instead of %v", n, expected)
}
}
}
func BenchmarkParseInt32Binary(b *testing.B) {
buf := make([]byte, 4)
binary.BigEndian.PutUint32(buf, 12345678)
expected := int32(12345678)
for i := 0; i < b.N; i++ {
n := int32(binary.BigEndian.Uint32(buf))
if n != expected {
b.Fatalf("Got %v instead of %v", n, expected)
}
}
}
func BenchmarkParseTimeText(b *testing.B) {
s := "2011-10-25 09:12:34.345921-05"
expected, _ := time.Parse("2006-01-02 15:04:05.999999-07", s)
for i := 0; i < b.N; i++ {
t, err := time.Parse("2006-01-02 15:04:05.999999-07", s)
if err != nil {
b.Fatalf("time.Parse failed: %v", err)
}
if t != expected {
b.Fatalf("time.Parse decoded %v instead of %v", t, expected)
}
}
}
// PostgreSQL binary format is an int64 of the number of microseconds since Y2K
func BenchmarkParseTimeBinary(b *testing.B) {
microsecFromUnixEpochToY2K := int64(946684800 * 1000000)
s := "2011-10-25 09:12:34.345921-05"
expected, _ := time.Parse("2006-01-02 15:04:05.999999-07", s)
microsecSinceUnixEpoch := expected.Unix()*1000000 + int64(expected.Nanosecond())/1000
microsecSinceY2K := microsecSinceUnixEpoch - microsecFromUnixEpochToY2K
buf := make([]byte, 8)
binary.BigEndian.PutUint64(buf, uint64(microsecSinceY2K))
for i := 0; i < b.N; i++ {
microsecSinceY2K := int64(binary.BigEndian.Uint64(buf))
microsecSinceUnixEpoch := microsecFromUnixEpochToY2K + microsecSinceY2K
t := time.Unix(microsecSinceUnixEpoch/1000000, (microsecSinceUnixEpoch%1000000)*1000)
if t != expected {
b.Fatalf("Got %v instead of %v", t, expected)
}
}
}
jack@hk-47~/dev/go/src/github.com/jackc/go_pgx_perf_observations$ go test -test.bench=Parse
testing: warning: no tests to run
PASS
BenchmarkParseInt32Text 50000000 62.8 ns/op
BenchmarkParseInt32Binary 500000000 3.40 ns/op
BenchmarkParseTimeText 2000000 775 ns/op
BenchmarkParseTimeBinary 100000000 15.4 ns/op
ok github.com/jackc/go_pgx_perf_observations 9.159s
解析 int32 比從文本解析比簡單地讀取二進制文件要多 18 倍。解析時間花費了 84 倍多的時間。絕對數字很小,但它們加起來。一般來說,二進制協議比文本協議快得多。
更多二進制技巧
使用 binary.Read 和 io.Reader 或 binary.Write 和 io.Writer 讀取或寫入二進制流時非常方便。但是直接使用 []byte 和 binary.BigEndian.Get* 或 binary.BigEndian.Put* 效率更高。
func BenchmarkBinaryWrite(b *testing.B) {
buf := &bytes.Buffer{}
for i := 0; i < b.N; i++ {
buf.Reset()
for j := 0; j < 10; j++ {
binary.Write(buf, binary.BigEndian, int32(j))
}
}
}
func BenchmarkBinaryPut(b *testing.B) {
var writebuf [1024]byte
for i := 0; i < b.N; i++ {
buf := writebuf[0:0]
for j := 0; j < 10; j++ {
b := make([]byte, 4)
binary.BigEndian.PutUint32(b, uint32(j))
buf = append(buf, b...)
}
}
}
jack@hk-47~/dev/go/src/github.com/jackc/go_pgx_perf_observations$ go test -test.bench=BenchmarkBinary -test.benchmem
testing: warning: no tests to run
PASS
BenchmarkBinaryWrite 1000000 1075 ns/op 80 B/op 5 allocs/op
BenchmarkBinaryPut 20000000 113 ns/op 0 B/op 0 allocs/op
ok github.com/jackc/go_pgx_perf_observations 3.485s
binary.Write 不僅慢得多,而且還會導致額外的內存分配。正是這一變化對 pgx 性能進行了實質性的改進。
最後測量
在提交優化之前,讓我以另一個要衡量的警告作爲結束。我想優化的一個用例是一個 Web API,它爲直接在 PostgreSQL 中生成的 JSON 提供服務。這樣做的正常方法是將 JSON 讀入一個字符串,然後將該字符串寫入 HTTP io.Writer。但是直接從 PostgreSQL io.Reader 複製到 HTTP io.Writer 不是更快嗎?很明顯它應該更快,但不幸的是它是不正確的。基準測試表明,在絕大多數情況下,它實際上速度較慢,而在最好的情況下,速度只會稍微快一點。
來源:
https://www.toutiao.com/article/7204769433263653413/?log_from=6115791aac80d_1678154276265
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/VsxQPETk6ZuU_g4sk0TP4A