可觀察性與性能在 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