Performans Analizi
Bir program yazıldıktan sonra, ondan sadece çalışmasını değil, aynı zamanda stabil ve verimli bir uygulama olmasını da bekleriz. Çeşitli testler yoluyla, programın çoğu stabilitesini garanti edebiliriz. Programın verimli olup olmadığı ise performans analizi yapmamızı gerektirir. Önceki içerikte, performans analizinin tek aracı Benchmark ile belirli bir fonksiyon biriminin ortalama yürütme süresini, bellek tahsisi durumunu vb. test etmekti. Ancak gerçekte program performans analizi için gereksinimler bundan çok daha fazlasıdır. Bazen programın genel CPU kullanımını, bellek kullanımını, yığın tahsis durumunu, goroutine durumunu, hotspot kod yollarını vb. analiz etmemiz gerekir. Bu Benchmark'ın karşılayamayacağı bir durumdur. Neyse ki go araç zinciri, geliştiricilerin kullanımı için birçok performans analiz aracı entegre etmiştir. Aşağıda tek tek açıklanacaktır.
Kaçış Analizi
Go'da, değişkenlerin bellek tahsisi derleyici tarafından belirlenir. Genellikle yığın (stack) ve heap olmak üzere iki yere tahsis edilir. Eğer heap'e tahsis edilmesi gereken bir değişken heap'e tahsis edilirse, bu duruma kaçış (escape) denir. Kaçış analizi programdaki bellek tahsis durumunu analiz etmektir. Derleme zamanında yapıldığı için statik analizin bir türüdür.
::: ipucu
Go'nun belleği nasıl tahsis ettiğini öğrenmek için Bellek Tahsisi makalesine gidin.
:::
Yerel İşaretçi Referansı
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
}GetPerson fonksiyonunda mom değişkeni oluşturulur. Fonksiyon içinde oluşturulduğu için aslında yığına tahsis edilmeliydi. Ancak son'un Mom alanı tarafından referans alınır ve son fonksiyon dönüş değeri olarak dışarı döndürülür. Bu nedenle derleyici bunu heap'e tahsis eder. Bu çok basit bir örnektir, bu yüzden anlamak için çok fazla çaba gerektirmez. Ancak daha büyük bir projede, kod satırı on binlerce olduğunda, manuel analiz o kadar kolay olmaz. Bu nedenle analiz için araç kullanmak gerekir. Yukarıda belirtildiği gibi bellek tahsisi derleyici tarafından yönetildiğinden, kaçış analizi de derleyici tarafından tamamlanır. Kullanımı çok basittir, aşağıdaki komutu çalıştırmanız yeterlidir:
$ go build -gcflags="-m -m -l"gcflags yani derleyici gc'nin parametreleri:
-m, kod optimizasyon önerilerini yazdırır, iki tane kullanıldığında daha detaylı çıktı verir-l, satır içi optimizasyonu devre dışı bırakır
Çıktı şu şekildedir
$ 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: momDerleyici açıkça mom değişkeninin kaçış yaptığını ve nedeninin dönüş değerinin fonksiyon içindeki yerel işaretçiyi içermesi olduğunu söyler. Bu durumun dışında kaçış fenomeni oluşabilecek diğer durumlar şunlardır:
::: ipuçları
Eğer kaçış analizi detayları ile ilgileniyorsanız, standart kütüphane cmd/compile/internal/escape/escape.go içinde daha fazla içerik öğrenebilirsiniz.
:::
Closure Referansı
Closure fonksiyon dışındaki bir değişkeni referans alırsa, bu değişken de heap'e kaçar. Bu anlaşılması kolaydır.
package main
func main() {
a := make([]string, 0)
do(func() []string {
return a
})
}
func do(f func() []string) []string {
return f()
}Çıktı
$ 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 escapeYetersiz Alan
Yığın alanı yetersiz olduğunda da kaçış fenomeni oluşur. Aşağıda oluşturulan slice 1<<15 kapasitesi talep eder
package main
func main() {
_ = make([]int, 0, 1<<15)
}Çıktı
$ 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 heapBilinmeyen Uzunluk
Slice'ın uzunluğu bir değişken olduğunda, uzunluğu bilinmediğinden kaçış fenomeni oluşur (map oluşmaz)
package main
func main() {
n := 100
_ = make([]int, n)
}Çıktı
$ 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 heapBir diğer özel durum ise fonksiyon parametresi ...any türü olduğunda da kaçış fenomeni oluşabilir
package main
import "fmt"
func main() {
n := 100
fmt.Println(n)
}Çıktı
$ 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 heapKaçış analizi yapmamızın nedeni, bellek tahsisini bu kadar detaylı kontrol etmemiz esas olarak GC baskısını azaltmaktır. Ancak go c dili değildir, bellek tahsisinin nihai kararı hala derleyicinin elindedir. Aşırı performans gereksinimleri durumları dışında, çoğu zaman bellek tahsisi detaylarına çok fazla odaklanmamıza gerek yoktur. Sonuçta GC'nin ortaya çıkış amacı geliştiricileri özgürleştirmektir.
::: ipucu Küçük Detay
Bazı referans türleri için, artık kullanılmayacağı onaylandıktan sonra, GC'ye bunu geri alabileceğini söylemek için nil olarak ayarlayabiliriz.
type Writer struct {
buf []byte
}
func (w Writer) Close() error {
w.buff = nil
return nil
}:::
pprof
pprof (program profiling), program performansı analizi için güçlü bir araçtır. Program çalışma zamanı verilerinin bir kısmını örnekleme yapar. CPU, bellek, goroutine, kilit, yığın bilgileri ve birçok diğer yönü kapsar. Ardından örnekleme verilerini analiz etmek ve sonuçları göstermek için araçlar kullanılır.
Bu nedenle pprof kullanım adımları sadece iki adımdır:
- Veri toplama
- Sonuç analizi
Toplama
Veri toplamanın iki yolu vardır: otomatik ve manuel. Her ikisinin de avantajları ve dezavantajları vardır. Bundan önce, bellek ve cpu tüketimini simüle etmek için basit bir fonksiyon yazalım
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)
}Manuel
Manuel toplama kod ile kontrol edilir. Avantajı kontrollü, esnek ve özelleştirilebilir olmasıdır. Kodda doğrudan pprof kullanmak için runtime/pprof paketini import etmeniz gerekir
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)
}
}pprof.Lookup tarafından desteklenen parametreler aşağıdaki kodda gösterildiği gibidir
profiles.m = map[string]*Profile{
"goroutine": goroutineProfile,
"threadcreate": threadcreateProfile,
"heap": heapProfile,
"allocs": allocsProfile,
"block": blockProfile,
"mutex": mutexProfile,
}Bu fonksiyon toplanan verileri belirtilen dosyaya yazar. Yazma sırasında geçirilen sayının birkaç anlamı vardır:
0, sıkıştırılmış Protobuf verilerini yazar, okunabilirliği yoktur1, metin formatında veri yazar, okunabilir, http arayüzü bu tür verileri döndürür2, sadecegoroutineiçin kullanılabilir,panicstili yığın bilgilerini yazdırır anlamına gelir
CPU verilerini toplamak için ayrı olarak pprof.StartCPUProfile fonksiyonu kullanılır. Örnekleme için belirli bir süre gerekir ve ham veriler okunamaz. Aşağıda gösterildiği gibi
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()
}Trace verilerini toplamak da aynı şekildedir
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()
}Otomatik
net/http/pprof paketi yukarıdaki analiz fonksiyonlarını http arayüzlerine paketler ve varsayılan rotaya kaydeder. Aşağıda gösterildiği gibi
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)
}Bu, pprof veri toplamayı tek tuşla çalıştırmamıza olanak tanır
package main
import (
"net/http"
// Bu paketi import etmeyi unutmayın
_ "net/http/pprof"
)
func main() {
go func(){
http.ListenAndServe(":8080", nil)
}
for {
Do()
}
}Bu noktada tarayıcıda http://127.0.0.1:8080/debug/pprof adresini ziyaret ederseniz, böyle bir sayfa görünür

Sayfada birkaç seçenek bulunur. Şunları temsil ederler:
allocs: Bellek tahsisi örneklemeblock: Senkronizasyon ilkelilerinin engelleme takibicmdline: Mevcut programın komut satırı çağrısıgoroutine: Tüm goroutine'leri takip etheap: Canlı nesnelerin bellek tahsisi örneklememutex: Karşılıklı kilit ile ilgili bilgi takibiprofile: CPU analizi, bir süre analiz eder ve bir dosya indirirthreadcreate: Yeni OS iş parçacığı oluşturma nedenlerinin analizitrace: Mevcut program yürütme durumunun takibi, aynı zamanda bir dosya indirir
Buradaki verilerin çoğunun okunabilirliği yüksek değildir. Esas olarak araç analizi için kullanılır. Aşağıdaki resimde gösterildiği gibi

Spesifik analiz çalışması daha sonra yapılacaktır. profile ve trace iki seçeneği dışında, web sayfasında veri dosyasını indirmek istiyorsanız, query parametresi debug=1'i kaldırabilirsiniz. Bu arayüzleri varsayılan rota yerine kendi rotanıza da entegre edebilirsiniz. Aşağıda gösterildiği gibi
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()
}Bu şekilde, diğer web framework'lerine de entegre edilebilir. Örneğin gin, iris vb.
Analiz
Toplanan veri dosyalarını aldıktan sonra, analiz etmek için iki yol vardır: komut satırı veya web sayfası. Her ikisi de pprof komut satırı aracını kullanır. go bu aracı varsayılan olarak entegre eder, bu nedenle ekstra indirme gerekmez.
pprof açık kaynak adresi: google/pprof: pprof is a tool for visualization and analysis of profiling data (github.com)
Komut Satırı
Toplanan veri dosyasını parametre olarak kullanın
$ go tool pprof heap.pbEğer veriler web tarafından toplanmışsa, dosya adı yerine web url kullanın.
$ go tool pprof -http :8080 http://127.0.0.1/debug/pprof/heapArdından etkileşimli bir komut satırı görünür
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)help yazarak diğer komutları görüntüleyebilirsiniz
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
...Komut satırında verilere bakmak için genellikle top komutu kullanılır. traces komutu da kullanılabilir ancak çıktısı çok uzundur. top komutu sadece genel bir bakış için kullanılır.
(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.mainBuradaki bazı göstergeleri kısaca tanıtayım (cpu için de aynı):
flat, mevcut fonksiyonun tükettiği kaynağı temsil edercum, mevcut fonksiyon ve sonraki çağrı zincirinin tükettiği toplam kaynakflat%, flat/totalcum%, cum/total
Tüm çağrı yığınının bellek kullanımının 117.49MB olduğunu açıkça görebiliriz. Do fonksiyonu kendisi hiçbir şey yapmadığından, sadece diğer fonksiyonları çağırdığından, flat göstergesi 0'dır. Slice oluşturma işi makeSlice fonksiyonu tarafından yapıldığından, flat göstergesi 100%'dür.
Görselleştirme formatına dönüştürebiliriz. pprof oldukça fazla formatı destekler. Örneğin pdf, svg, png, gif vb. (Graphviz yüklenmesi gerekir).
(pprof) png
Generating report in profile001.png
Resim aracılığıyla tüm çağrı yığınının bellek durumunu daha net görebiliriz.
Kaynak kodu şeklinde görüntülemek için list komutunu kullanabiliriz
(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 {Resim ve kaynak kodu için, web ve weblist komutları ile tarayıcıda resim ve kaynak kodu görüntülenebilir.
Web Sayfası
Bundan önce, verilerin daha çeşitli olması için simüle edilen fonksiyonu biraz değiştirelim
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)
}Web analizi sonuçları görselleştirebilir, komut satırını manuel olarak çalıştırmaktan kurtarır. Web analizi kullanırken, sadece aşağıdaki komutu çalıştırmanız gerekir
$ go tool pprof -http :8080 heap.pbEğer veriler web tarafından toplanmışsa, dosya adı yerine web url kullanın
$ 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::: ipucu
Verilerin nasıl analiz edileceği hakkında daha fazla bilgi için pprof: How to read the graph adresine gidin
:::

Web sayfasında toplam 6 görüntülenebilir öğe vardır:
- Top, komut top ile aynı
- Graph, çizgi grafik
- Flame Graph, alev grafiği
- Peek,
- Source, kaynak kodu görüntüleme
- Disassemble, tersine montaj görüntüleme
Bellek için dört boyut analiz edilebilir:
alloc_objects: Şu ana kadar tahsis edilen tüm nesne sayısı, serbest bırakılanlar dahilalloc_spcae: Şu ana kadar tahsis edilen tüm bellek alanı, serbest bırakılanlar dahilinuse_objects: Kullanımda olan nesne sayısıinuse_space: Kullanımda olan bellek alanı

Yukarıdaki resimde en alttaki beyaz yaprak düğümler farklı boyuttaki nesnelerin kullanımını temsil eder.

Çizgi grafik için dikkat edilmesi gereken birkaç nokta vardır:
- Blokların rengi ne kadar koyu ise, kullanım o kadar yüksektir. Çizgi ne kadar kalınsa, kullanım o kadar yüksektir
- Düz çizgi doğrudan çağrıyı temsil eder, kesikli çizgi bazı çağrı zincirlerinin atlandığını temsil eder.


Alev grafiği için, yukarıdan aşağıya bakıldığında çağrı zinciri, soldan sağa bakıldığında cum kullanım yüzdesidir.
trace
pprof esas olarak programın kaynak kullanımını analiz etmekten sorumludur. trace ise programın çalışma detaylarını takip etmek için daha uygundur. Veri dosyası önceki ile uyumlu değildir. go tool trace komutu ile ilgili analiz çalışmaları tamamlanır.
Eğer manuel olarak toplanan verilerse, dosya adı parametre olarak kullanılabilir
$ go tool trace trace.outEğer otomatik olarak toplanmışsa, aynı mantık geçerlidir
$ curl http://127.0.0.1:8080/debug/pprof/trace > trace.out && go tool trace trace.outÇalıştırıldıktan sonra bir web sunucusu açılır
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:51805Açtıktan sonra sayfa yaklaşık olarak şu şekildedir

Burada esas olarak aşağıdaki bölümler bulunur. Bu verileri anlamak oldukça kolay değildir.
Event timelines for running goroutines
trace by proc: O anda bu işlemcide çalışan goroutine zaman çizelgesini gösterir

trace by thread: O anda OS iş parçacığında çalışan goroutine zaman çizelgesini gösterir

Goroutine analysis: Her grup ana fonksiyonun goroutine ile ilgili istatistik bilgilerini gösterir


Profiles
- Network blocking profile: Ağ IO nedeniyle engellenen goroutine bilgileri
- Synchronization blocking profile: Senkronizasyon ilkesi nedeniyle engellenen goroutine bilgileri
- Syscall profile: Sistem çağrısı nedeniyle engellenen goroutine bilgileri
User-defined tasks and regions
- User-defined tasks: Kullanıcı tanımlı görevlerin ilgili goroutine bilgileri
- User-defined regions: Kullanıcı tanımlı kod bölgelerinin ilgili goroutine bilgileri
Garbage collection metrics
Minimum mutator utilization: Son GC'nin maksimum zaman aşımını gösterir

