Go 高性能 - 打印調用堆棧
概述
在工程代碼中需要在異常場景打印相應的日誌,記錄重要的上下文信息。如果遇到 panic
或 error
的情況, 這時候就需要詳細的 堆棧信息
作爲輔助來排查問題,本小節就來介紹兩種常見的獲取 堆棧信息
方法, 然後對兩種方法進行基準測試,最後使用測試的結果進行性能對比並分析差異。
runtime.Stack
通過標準庫提供的 runtime.Stack
相關 API 來獲取。
示例
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
# 輸出如下 (你的輸出代碼路徑應該和這裏的不一樣)
goroutine 1 [running]:
main.main()
/home/codes/go-high-performance/main.go:10 +0x45
...
測試代碼如下
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]
}
}
運行測試,並將基準測試結果寫入文件:
# 運行 1000 次,統計內存分配
$ go test -run='^$' -bench=. -count=1 -benchtime=1000x -benchmem > slow.txt
runtime.Caller
通過標準庫提供的 runtime.Caller
相關 API 來獲取。
示例
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
# 輸出如下 (你的輸出代碼路徑應該和這裏的不一樣)
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
...
從輸出的結果中可以看到,runtime.Caller
的返回值包含了 文件名稱
和 行號
,但是相比 runtime.Stack
的輸出而言, 缺少了 goroutine
和 調用方法
字段,我們可以通過 runtime.Callers
配合 runtime.CallersFrames
輸出和 runtime.Stack
一樣的結果。
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
# 輸出如下 (你的輸出代碼路徑應該和這裏的不一樣)
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
...
測試代碼
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()
}
}
運行測試,並將基準測試結果寫入文件:
# 運行 1000 次,統計內存分配
$ 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)
輸出的結果分爲了三行,分別對應基準測試期間的: 運行時間、內存分配總量、內存分配次數,可以看到:
-
• 運行時間:
runtime.Callers
比runtime.Stack
提升了將近30 倍
-
• 內存分配總量: 兩者差不多
-
• 內存分配次數:
runtime.Callers
比runtime.Stack
降低了將近10 倍
,當然筆者的測試代碼也需要再優化下
性能分析
最根本的差異點在於
runtime.Stack
會觸發STW
操作。
小結
本小節介紹了兩種獲取堆棧信息的方法,並通過基準測試來分析兩種方法的性能差異,讀者可以在此基礎上封裝自己的高性能組件類庫。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/saIRop2wq87sJQ2rbY1t5g