Skip to content

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

go
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:

bash
$ 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

bash
$ 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: mom

Compiler 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.

go
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 escape

Ruang Tidak Cukup

Saat ruang stack tidak cukup, juga akan terjadi fenomena escape, berikut membuat slice mengapply kapasitas 1<<15

go
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 heap

Panjang Tidak Diketahui

Saat panjang slice adalah variabel, karena panjangnya tidak diketahui, akan terjadi fenomena escape (map tidak akan)

go
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 heap

Ada satu situasi khusus yaitu saat parameter fungsi adalah tipe ...any juga mungkin terjadi escape

go
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 heap

Alasan 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.

go
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:

  1. Mengumpulkan data
  2. 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

go
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

go
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

go
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 readability
  • 1, menulis data format teks, dapat dibaca, http interface mengembalikan data jenis ini
  • 2, hanya goroutine yang tersedia,表示 mencetak informasi stack gaya panic

Mengumpulkan data cpu perlu menggunakan fungsi pprof.StartCPUProfile secara terpisah, ia memerlukan waktu tertentu untuk sampling, dan data mentahnya tidak dapat dibaca, sebagai berikut

go
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

go
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

go
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

go
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 memori
  • block: tracking blocking synchronization primitives
  • cmdline: command line call program saat ini
  • goroutine: tracking semua goroutine
  • heap: sampling alokasi memori untuk objek yang masih hidup
  • mutex: tracking informasi terkait mutex lock
  • profile: analisis cpu, akan menganalisis beberapa waktu dan mendownload file
  • threadcreate: analisis penyebab pembuatan thread OS baru
  • trace: 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

go
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

bash
$ go tool pprof heap.pb

Jika data dikumpulkan oleh web, gunakan web url menggantikan nama file.

bash
$ go tool pprof -http :8080 http://127.0.0.1/debug/pprof/heap

Lalu akan muncul command line interaktif

bash
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.main

Perkenalkan beberapa indikator di antaranya (cpu sama)

  • flat, mewakili resource yang dikonsumsi fungsi saat ini
  • cum, total resource yang dikonsumsi fungsi saat ini dan rantai panggilan berikutnya
  • flat%, flat/total
  • cum%, 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

go
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

bash
$ go tool pprof -http :8080 heap.pb

Jika data dikumpulkan oleh web, ganti web url dengan nama file

bash
$ 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/goroutine

TIP

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 direlease
  • alloc_spcae: total ruang memori yang telah dialokasikan sejauh ini, termasuk yang sudah direlease
  • inuse_objects: jumlah objek yang sedang digunakan
  • inuse_space: ruang memori yang sedang digunakan

Grafik Analisis Memori

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

Grafik Analisis CPU

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.

Flame Graph Memori

Flame Graph CPU

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.out

Jika dikumpulkan otomatis, juga sama

bash
$ curl http://127.0.0.1:8080/debug/pprof/trace > trace.out && go tool trace trace.out

Setelah 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:51805

Buka 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

Golang by www.golangdev.cn edit