本小節(jié)介紹了兩種獲取堆棧信息的方法,并通過(guò)基準(zhǔn)測(cè)試來(lái)分析兩種方法的性能差異,讀者可以在此基礎(chǔ)上封裝自己的高性能組件類庫(kù)。
概述
在工程代碼中需要在異常場(chǎng)景打印相應(yīng)的日志,記錄重要的上下文信息。如果遇到 panic 或 error 的情況, 這時(shí)候就需要詳細(xì)的 堆棧信息 作為輔助來(lái)排查問(wèn)題,本小節(jié)就來(lái)介紹兩種常見(jiàn)的獲取 堆棧信息 方法, 然后對(duì)兩種方法進(jìn)行基準(zhǔn)測(cè)試,最后使用測(cè)試的結(jié)果進(jìn)行性能對(duì)比并分析差異。
runtime.Stack
通過(guò)標(biāo)準(zhǔn)庫(kù)提供的 runtime.Stack 相關(guān) API 來(lái)獲取。
示例
package main
import (
"fmt"
"runtime"
)
func main() {
buf := make([]byte, 1024)
n := runtime.Stack(buf, true)
fmt.Printf("%s\n", buf[:n])
}
$ go run main.go
# 輸出如下 (你的輸出代碼路徑應(yīng)該和這里的不一樣)
goroutine 1 [running]:
main.main()
/home/codes/go-high-performance/main.go:10 +0x45
...
測(cè)試代碼如下
package performance
import (
"runtime"
"testing"
)
func Benchmark_StackDump(b *testing.B) {
for i := 0; i < b.N; i++ {
buf := make([]byte, 1024)
n := runtime.Stack(buf, true)
_ = buf[:n]
}
}
運(yùn)行測(cè)試,并將基準(zhǔn)測(cè)試結(jié)果寫(xiě)入文件:
# 運(yùn)行 1000 次,統(tǒng)計(jì)內(nèi)存分配
$ go test -run='^$' -bench=. -count=1 -benchtime=1000x -benchmem > slow.txt
runtime.Caller
通過(guò)標(biāo)準(zhǔn)庫(kù)提供的 runtime.Caller 相關(guān) API 來(lái)獲取。
示例
package main
import (
"fmt"
"runtime"
)
func main() {
for i := 0; ; i++ {
if _, file, line, ok := runtime.Caller(i); ok {
fmt.Printf("file: %s, line: %d\n", file, line)
} else {
break
}
}
}
$ go run main.go
# 輸出如下 (你的輸出代碼路徑應(yīng)該和這里的不一樣)
file: /home/codes/go-high-performance/main.go, line: 10
file: /usr/local/go/src/runtime/proc.go, line: 250
file: /usr/local/go/src/runtime/asm_amd64.s, line: 1594
...
從輸出的結(jié)果中可以看到,runtime.Caller 的返回值包含了 文件名稱 和 行號(hào),但是相比 runtime.Stack 的輸出而言, 缺少了 goroutine 和 調(diào)用方法 字段,我們可以通過(guò) runtime.Callers 配合 runtime.CallersFrames 輸出和 runtime.Stack 一樣的結(jié)果。
package main
import (
"fmt"
"runtime"
"strconv"
"strings"
)
func main() {
pcs := make([]uintptr, 16)
n := runtime.Callers(0, pcs)
frames := runtime.CallersFrames(pcs[:n])
var sb strings.Builder
for {
frame, more := frames.Next()
sb.WriteString(frame.Function)
sb.WriteByte('\n')
sb.WriteByte('\t')
sb.WriteString(frame.File)
sb.WriteByte(':')
sb.WriteString(strconv.Itoa(frame.Line))
sb.WriteByte('\n')
if !more {
break
}
}
fmt.Println(sb.String())
}
$ go run main.go
# 輸出如下 (你的輸出代碼路徑應(yīng)該和這里的不一樣)
runtime.Callers
/usr/local/go/src/runtime/extern.go:247
main.main
/home/codes/go-high-performance/main.go:12
runtime.main
/usr/local/go/src/runtime/proc.go:250
runtime.goexit
/usr/local/go/src/runtime/asm_amd64.s:1594
...
測(cè)試代碼
package performance
import (
"runtime"
"strconv"
"strings"
"testing"
)
func stackDump() string {
pcs := make([]uintptr, 16)
n := runtime.Callers(0, pcs)
frames := runtime.CallersFrames(pcs[:n])
var buffer strings.Builder
for {
frame, more := frames.Next()
buffer.WriteString(frame.Function)
buffer.WriteByte('\n')
buffer.WriteByte('\t')
buffer.WriteString(frame.File)
buffer.WriteByte(':')
buffer.WriteString(strconv.Itoa(frame.Line))
buffer.WriteByte('\n')
if !more {
break
}
}
return buffer.String()
}
func Benchmark_StackDump(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = stackDump()
}
}
運(yùn)行測(cè)試,并將基準(zhǔn)測(cè)試結(jié)果寫(xiě)入文件:
# 運(yùn)行 1000 次,統(tǒng)計(jì)內(nèi)存分配
$ go test -run='^$' -bench=. -count=1 -benchtime=1000x -benchmem > fast.txt
使用 benchstat 比較差異
$ benchstat -alpha=100 fast.txt slow.txt
# 輸出如下
name old time/op new time/op delta
_StackDump-8 2.28μs ± 0% 68.89μs ± 0% +2926.85% (p=1.000 n=1+1)
name old alloc/op new alloc/op delta
_StackDump-8 1.36kB ± 0% 1.02kB ± 0% -24.71% (p=1.000 n=1+1)
name old allocs/op new allocs/op delta
_StackDump-8 12.0 ± 0% 1.0 ± 0% -91.67% (p=1.000 n=1+1)
輸出的結(jié)果分為了三行,分別對(duì)應(yīng)基準(zhǔn)測(cè)試期間的: 運(yùn)行時(shí)間、內(nèi)存分配總量、內(nèi)存分配次數(shù),可以看到:
- ? 運(yùn)行時(shí)間: runtime.Callers 比 runtime.Stack 提升了將近 30 倍
- ? 內(nèi)存分配總量: 兩者差不多
- ? 內(nèi)存分配次數(shù): runtime.Callers 比 runtime.Stack 降低了將近 10 倍,當(dāng)然筆者的測(cè)試代碼也需要再優(yōu)化下
性能分析
最根本的差異點(diǎn)在于 runtime.Stack 會(huì)觸發(fā) STW 操作。
小結(jié)
本小節(jié)介紹了兩種獲取堆棧信息的方法,并通過(guò)基準(zhǔn)測(cè)試來(lái)分析兩種方法的性能差異,讀者可以在此基礎(chǔ)上封裝自己的高性能組件類庫(kù)。