Analisis Performa
Setelah program ditulis, tuntutan kita terhadapnya tidak hanya bisa berjalan, tetapi juga berharap ia adalah aplikasi yang stabil dan efisien. Melalui berbagai testing, kita dapat menjamin sebagian besar stabilitas program, sedangkan apakah program efisien, perlu kita melakukan analisis performa terhadapnya. Di konten sebelumnya, satu-satunya cara analisis performa program hanya melalui Benchmark untuk menguji rata-rata waktu eksekusi unit fungsi tertentu, situasi alokasi memori dll, namun realitas analisis performa program tuntutan jauh lebih dari ini, terkadang kita perlu menganalisis okupansi CPU keseluruhan program, okupansi memori, situasi alokasi heap, status goroutine, jalur kode hot point dll, ini adalah yang tidak dapat dipenuhi Benchmark. Untungnya toolchain go mengintegrasikan banyak tool analisis performa untuk digunakan developer, berikut akan menjelaskan satu per satu.
Analisis Escape
Di go, alokasi memori variabel ditentukan oleh compiler, umumnya dialokasikan di stack dan heap. Jika variabel yang seharusnya dialokasikan di stack dialokasikan ke heap, situasi ini disebut escape, analisis escape adalah menganalisis situasi alokasi memori dalam program. Karena dilakukan saat compile time, jadi termasuk analisis statis.
TIP
前往 artikel Alokasi Memori untuk了解 bagaimana go mengalokasikan memori secara spesifik.
Referensi Pointer Lokal
package main
func main() {
GetPerson()
}
type Person struct {
Name string
Mom *Person
}
func GetPerson() Person {
mom := Person{Name: "lili"}
son := Person{Name: "jack", Mom: &mom}
return son
}Fungsi GetPerson membuat variabel mom, karena dibuat di dalam fungsi, seharusnya dialokasikan di stack, tetapi direferensikan oleh field Mom dari son, dan son dikembalikan sebagai nilai return fungsi, oleh karena itu compiler mengalokasikannya di heap. Ini adalah contoh yang sangat sederhana, jadi memahaminya tidak perlu banyak usaha, tetapi jika proyek yang lebih besar, baris kode ada puluhan ribu, analisis manual tidak lagi mudah, untuk ini perlu menggunakan tool untuk analisis escape. Sebelumnya disebutkan alokasi memori dipimpin oleh compiler, jadi analisis escape juga diselesaikan oleh compiler, penggunaannya sangat sederhana, cukup jalankan command berikut:
$ go build -gcflags="-m -m -l"gcflags yaitu parameter compiler gc,
-m, mencetak saran optimasi kode, sekaligus muncul dua akan lebih detail output-l, menonaktifkan optimasi inline
Output sebagai berikut
$ go build -gcflags="-m -m -l" .
# golearn/example
./main.go:13:2: mom escapes to heap:
./main.go:13:2: flow: son = &mom:
./main.go:13:2: from &mom (address-of) at ./main.go:14:35
./main.go:13:2: from Person{...} (struct literal element) at ./main.go:14:15
./main.go:13:2: from son := Person{...} (assign) at ./main.go:14:6
./main.go:13:2: flow: ~r0 = son:
./main.go:13:2: from return son (return) at ./main.go:15:2
./main.go:13:2: moved to heap: momCompiler dengan jelas memberitahu kita variabel mom terjadi escape, penyebabnya adalah karena nilai return berisi pointer lokal fungsi, selain situasi ini ada situasi lain yang mungkin terjadi fenomena escape
::: tips
Jika Anda tertarik dengan detail analisis escape, dapat了解 konten lebih lanjut di standard library cmd/compile/internal/escape/escape.go.
:::
Closure Reference
Closure mereferensikan variabel di luar fungsi, maka variabel tersebut juga akan escape ke heap, ini sangat mudah dipahami.
package main
func main() {
a := make([]string, 0)
do(func() []string {
return a
})
}
func do(f func() []string) []string {
return f()
}Output
$ go build -gcflags="-m -m -l" .
# golearn/example
./main.go:10:9: f does not escape
./main.go:4:2: main capturing by value: a (addr=false assign=false width=24)
./main.go:4:11: make([]string, 0) escapes to heap:
./main.go:4:11: flow: a = &{storage for make([]string, 0)}:
./main.go:4:11: from make([]string, 0) (spill) at ./main.go:4:11
./main.go:4:11: from a := make([]string, 0) (assign) at ./main.go:4:4
./main.go:4:11: flow: ~r0 = a:
./main.go:4:11: from return a (return) at ./main.go:6:3
./main.go:4:11: make([]string, 0) escapes to heap
./main.go:5:5: func literal does not escapeRuang Tidak Cukup
Saat ruang stack tidak cukup, juga akan terjadi fenomena escape, berikut membuat slice mengapply kapasitas 1<<15
package main
func main() {
_ = make([]int, 0, 1<<15)
}Output
$ go build -gcflags="-m -m -l" .
# golearn/example
./main.go:4:10: make([]int, 0, 32768) escapes to heap:
./main.go:4:10: flow: {heap} = &{storage for make([]int, 0, 32768)}:
./main.go:4:10: from make([]int, 0, 32768) (too large for stack) at ./main.go:4:10
./main.go:4:10: make([]int, 0, 32768) escapes to heapPanjang Tidak Diketahui
Saat panjang slice adalah variabel, karena panjangnya tidak diketahui, akan terjadi fenomena escape (map tidak akan)
package main
func main() {
n := 100
_ = make([]int, n)
}Output
$ go build -gcflags="-m -m -l" .
# golearn/example
./main.go:5:10: make([]int, n) escapes to heap:
./main.go:5:10: flow: {heap} = &{storage for make([]int, n)}:
./main.go:5:10: from make([]int, n) (non-constant size) at ./main.go:5:10
./main.go:5:10: make([]int, n) escapes to heapAda satu situasi khusus yaitu saat parameter fungsi adalah tipe ...any juga mungkin terjadi escape
package main
import "fmt"
func main() {
n := 100
fmt.Println(n)
}Output
$ go build -gcflags="-m -m -l" .
# golearn/example
./main.go:7:14: n escapes to heap:
./main.go:7:14: flow: {storage for ... argument} = &{storage for n}:
./main.go:7:14: from n (spill) at ./main.go:7:14
./main.go:7:14: from ... argument (slice-literal-element) at ./main.go:7:13
./main.go:7:14: flow: {heap} = {storage for ... argument}:
./main.go:7:14: from ... argument (spill) at ./main.go:7:13
./main.go:7:14: from fmt.Println(... argument...) (call parameter) at ./main.go:7:13
./main.go:7:13: ... argument does not escape
./main.go:7:14: n escapes to heapAlasan kita melakukan analisis escape, mengontrol alokasi memori sedemikian detail, terutama untuk mengurangi tekanan GC, tetapi go bukan bahasa c, hak keputusan akhir alokasi memori masih di tangan compiler, kecuali situasi tuntutan performa yang ekstrem, sebagian besar waktu kita juga tidak perlu terlalu fokus pada detail alokasi memori, lagipula tujuan GC lahir adalah untuk membebaskan developer.
Detail Kecil
Untuk beberapa tipe reference, setelah konfirmasi tidak akan digunakan lagi, kita dapat mensetnya menjadi nil, untuk memberi tahu GC dapat mereclaimnya.
type Writer struct {
buf []byte
}
func (w Writer) Close() error {
w.buff = nil
return nil
}pprof
pprof (program profiling), adalah tool analisis performa program yang powerful, ia akan melakukan sampling sebagian data runtime program, mencakup cpu, memori, goroutine, lock, informasi stack dan banyak aspek, lalu menggunakan tool untuk menganalisis data yang disampling dan menampilkan hasil.
Jadi langkah penggunaan pprof hanya dua:
- Mengumpulkan data
- Menganalisis hasil
Pengumpulan
Cara pengumpulan data ada dua, otomatis dan manual, masing-masing ada kelebihan dan kekurangan. Sebelum ini, tulis fungsi sederhana untuk mensimulasikan konsumsi memori dan cpu
func Do() {
for i := 0; i < 10; i++ {
slice := makeSlice()
sortSlice(slice)
}
}
func makeSlice() []int {
var s []int
for range 1 << 24 {
s = append(s, rand.Int())
}
return s
}
func sortSlice(s []int) {
slices.Sort(s)
}Manual
Pengumpulan manual adalah dikontrol melalui kode, keuntungannya adalah可控, fleksibel, dapat dikustomisasi, langsung menggunakan pprof di kode perlu mengimport paket runtime/pprof
package main
import (
"log"
"os"
"runtime/pprof"
)
func main() {
Do()
w, _ := os.Create("heap.pb")
heapProfile := pprof.Lookup("heap")
err := heapProfile.WriteTo(w, 0)
if err != nil {
log.Fatal(err)
}
}Parameter yang didukung pprof.Lookup seperti kode berikut
profiles.m = map[string]*Profile{
"goroutine": goroutineProfile,
"threadcreate": threadcreateProfile,
"heap": heapProfile,
"allocs": allocsProfile,
"block": blockProfile,
"mutex": mutexProfile,
}Fungsi ini akan menulis data yang dikumpulkan ke file yang ditentukan, saat menulis angka yang传入 memiliki beberapa makna berikut
0, menulis data Protobuf yang dikompresi, tidak ada readability1, menulis data format teks, dapat dibaca, http interface mengembalikan data jenis ini2, hanyagoroutineyang tersedia,表示 mencetak informasi stack gayapanic
Mengumpulkan data cpu perlu menggunakan fungsi pprof.StartCPUProfile secara terpisah, ia memerlukan waktu tertentu untuk sampling, dan data mentahnya tidak dapat dibaca, sebagai berikut
package main
import (
"log"
"os"
"runtime/pprof"
"time"
)
func main() {
Do()
w, _ := os.Create("cpu.out")
err := pprof.StartCPUProfile(w)
if err != nil {
log.Fatal(err)
}
time.Sleep(time.Second * 10)
pprof.StopCPUProfile()
}Mengumpulkan data trace juga sama
package main
import (
"log"
"os"
"runtime/trace"
"time"
)
func main() {
Do()
w, _ := os.Create("trace.out")
err := trace.Start(w)
if err != nil {
log.Fatal(err)
}
time.Sleep(time.Second * 10)
trace.Stop()
}Otomatis
Paket net/http/pprof mengemas fungsi analisis di atas menjadi http interface, dan mendaftarkannya ke route default, sebagai berikut
package pprof
import ...
func init() {
http.HandleFunc("/debug/pprof/", Index)
http.HandleFunc("/debug/pprof/cmdline", Cmdline)
http.HandleFunc("/debug/pprof/profile", Profile)
http.HandleFunc("/debug/pprof/symbol", Symbol)
http.HandleFunc("/debug/pprof/trace", Trace)
}Ini memungkinkan kita langsung satu klik menjalankan pengumpulan data pprof
package main
import (
"net/http"
// Ingat untuk mengimport paket ini
_ "net/http/pprof"
)
func main() {
go func(){
http.ListenAndServe(":8080", nil)
}
for {
Do()
}
}Saat ini buka browser akses http://127.0.0.1:8080/debug/pprof, akan muncul halaman seperti ini

Di halaman ada beberapa opsi yang dapat dipilih, masing-masing mewakili
allocs: sampling alokasi memoriblock: tracking blocking synchronization primitivescmdline: command line call program saat inigoroutine: tracking semua goroutineheap: sampling alokasi memori untuk objek yang masih hidupmutex: tracking informasi terkait mutex lockprofile: analisis cpu, akan menganalisis beberapa waktu dan mendownload filethreadcreate: analisis penyebab pembuatan thread OS barutrace: tracking situasi eksekusi program saat ini, juga akan mendownload file
Data di sini sebagian besar readability tidak tinggi, terutama untuk tool analisis, seperti gambar

Pekerjaan analisis spesifik harus ditinggalkan untuk nanti, kecuali dua opsi profile dan trace, jika ingin mendownload file data di web, dapat menghapus parameter query debug=1. Juga dapat mengintegrasikan interface ini ke route sendiri bukan menggunakan route default, sebagai berikut
package main
import (
"net/http"
"net/http/pprof"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/trace", pprof.Trace)
servre := &http.Server{
Addr: ":8080",
Handler: mux,
}
servre.ListenAndServe()
}Dengan demikian, juga dapat mengintegrasikannya ke framework web lain, misalnya gin, iris dll.
Analisis
Setelah mendapatkan file data yang dikumpulkan, ada dua cara untuk analisis, command line atau web, keduanya perlu借助 tool command line pprof, go default mengintegrasikan tool ini, jadi tidak perlu download tambahan.
Source code terbuka pprof: google/pprof: pprof is a tool for visualization and analysis of profiling data (github.com)
Command Line
Jadikan file data yang dikumpulkan sebelumnya sebagai parameter
$ go tool pprof heap.pbJika data dikumpulkan oleh web, gunakan web url menggantikan nama file.
$ go tool pprof -http :8080 http://127.0.0.1/debug/pprof/heapLalu akan muncul command line interaktif
15:27:38.3266862 +0800 CST
Type: inuse_space
Time: Apr 15, 2024 at 3:27pm (CST)
No samples were found with the default sample value type.
Try "sample_index" command to analyze different sample values.
Entering interactive mode (type "help" for commands, "o" for options)
(pprof)Input help, dapat melihat command lain
Commands:
callgrind Outputs a graph in callgrind format
comments Output all profile comments
disasm Output assembly listings annotated with samples
dot Outputs a graph in DOT format
eog Visualize graph through eog
evince Visualize graph through evince
...Di command line melihat data umumnya menggunakan command top, juga bisa menggunakan traces tetapi outputnya sangat panjang, top command hanya untuk melihat sekilas.
(pprof) top 5
Showing nodes accounting for 117.49MB, 100% of 117.49MB total
flat flat% sum% cum cum%
117.49MB 100% 100% 117.49MB 100% main.makeSlice (inline)
0 0% 100% 117.49MB 100% main.Do
0 0% 100% 117.49MB 100% main.main
0 0% 100% 117.49MB 100% runtime.mainPerkenalkan beberapa indikator di antaranya (cpu sama)
flat, mewakili resource yang dikonsumsi fungsi saat inicum, total resource yang dikonsumsi fungsi saat ini dan rantai panggilan berikutnyaflat%, flat/totalcum%, cum/total
Kita dapat dengan jelas melihat okupansi memori seluruh call stack adalah 117.49MB, karena fungsi Do sendiri tidak melakukan apa-apa, hanya memanggil fungsi lain, jadi indikator flatnya adalah 0, pembuatan slice dilakukan oleh fungsi makeSlice, jadi indikator flatnya adalah 100%.
Kita dapat mengkonversi format visualisasi, pprof mendukung cukup banyak format, misalnya pdf, svg, png, gif dll (perlu instal Graphviz).
(pprof) png
Generating report in profile001.png
Melalui gambar kita dapat lebih jelas melihat situasi okupansi memori seluruh call stack.
Melalui list command melihat dalam bentuk source code
(pprof) list Do
Total: 117.49MB
ROUTINE ======================== main.Do in D:\WorkSpace\Code\GoLeran\golearn\example\main.go
0 117.49MB (flat, cum) 100% of Total
. . 21:func Do() {
. . 22: for i := 0; i < 10; i++ {
. 117.49MB 23: slice := makeSlice()
. . 24: sortSlice(slice)
. . 25: }
. . 26:}
. . 27:
. . 28:func makeSlice() []int {Untuk gambar dan source code, juga dapat menggunakan web dan weblist command untuk melihat gambar dan source code di browser.
Web
Sebelum ini agar data lebih beragam, modifikasi sedikit fungsi simulasi
func Do1() {
for i := 0; i < 10; i++ {
slice := makeSlice()
sortSlice(slice)
}
}
func Do2() {
for i := 0; i < 10; i++ {
slice := makeSlice()
sortSlice(slice)
}
}
func makeSlice() []int {
var s []int
for range 1 << 12 {
s = append(s, rand.Int())
}
return s
}
func sortSlice(s []int) {
slices.Sort(s)
}Analisis web dapat memvisualisasikan hasil,免去 kita mengoperasikan command line manual, saat menggunakan analisis web, cukup jalankan command berikut
$ go tool pprof -http :8080 heap.pbJika data dikumpulkan oleh web, ganti web url dengan nama file
$ go tool pprof -http :8080 http://127.0.0.1:9090/debug/pprof/heap
$ go tool pprof -http :8080 http://127.0.0.1:9090/debug/pprof/profile
$ go tool pprof -http :8080 http://127.0.0.1:9090/debug/pprof/goroutineTIP
Tentang cara menganalisis data,前往 pprof: How to read the graph untuk了解 lebih lanjut

Di web total ada 6 item yang dapat dilihat
- Top, sama dengan command top
- Graph, diagram garis
- Flame Graph, flame graph
- Peek,
- Source, melihat source code
- Disassemble, disassembly melihat
Untuk memori ada empat dimensi yang dapat dianalisis
alloc_objects: jumlah semua objek yang telah dialokasikan saat ini, termasuk yang sudah direleasealloc_spcae: total ruang memori yang telah dialokasikan sejauh ini, termasuk yang sudah direleaseinuse_objects: jumlah objek yang sedang digunakaninuse_space: ruang memori yang sedang digunakan

Node leaf putih di bagian paling bawah gambar mewakili objek dengan ukuran berbeda yang menempati.

Tentang diagram garis, ada beberapa poin yang perlu diperhatikan
- Warna blok semakin gelap, okupansi semakin tinggi, garis semakin tebal, okupansi semakin tinggi
- Garis solid mewakili direct call, garis putus-putus mewakili melewatkan beberapa call chain.


Untuk flame graph, melihat dari atas ke bawah adalah call chain, melihat dari kiri ke kanan adalah persentase okupansi cum.
trace
pprof terutama bertanggung jawab untuk menganalisis okupansi resource program, sedangkan trace lebih cocok untuk tracking detail eksekusi program, ia dengan file data yang前者 tidak kompatibel, diselesaikan oleh command go tool trace untuk pekerjaan analisis terkait.
Jika data dikumpulkan manual, dapat menggunakan nama file sebagai parameter
$ go tool trace trace.outJika dikumpulkan otomatis, juga sama
$ curl http://127.0.0.1:8080/debug/pprof/trace > trace.out && go tool trace trace.outSetelah eksekusi akan开启 web server
2024/04/15 17:15:40 Preparing trace for viewer...
2024/04/15 17:15:40 Splitting trace for viewer...
2024/04/15 17:15:40 Opening browser. Trace viewer is listening on http://127.0.0.1:51805Buka setelah halaman kira-kira sebagai berikut

Di dalamnya terutama mencakup beberapa bagian berikut, data ini untuk dipahami cukup tidak mudah.
Event timelines for running goroutines
trace by proc: menampilkan timeline goroutine yang berjalan di processor tersebut setiap saat

trace by thread: menampilkan timeline goroutine yang berjalan di thread OS setiap saat

Goroutine analysis: menampilkan informasi statistik terkait goroutine setiap grup fungsi utama


Profiles
- Network blocking profile: informasi goroutine yang blocking karena network IO
- Synchronization blocking profile: informasi goroutine yang blocking karena synchronization primitives
- Syscall profile: informasi goroutine yang blocking karena system call
User-defined tasks and regions
- User-defined tasks: informasi goroutine terkait task yang didefinisikan user
- User-defined regions: informasi goroutine terkait region kode yang didefinisikan user
Garbage collection metrics
Minimum mutator utilization: menampilkan durasi maksimum GC terbaru

