Optimasi yang dimulai dari intuisi hampir selalu salah alamat: loop yang terlihat mencurigakan sering hanya 2% dari total waktu, sementara pembunuh sebenarnya bersembunyi di fungsi yang tampak polos. CPU profile menghilangkan tebakan — perekam mengambil sampel stack secara berkala selama program berjalan, sehingga fungsi yang paling sering muncul di sampel adalah fungsi yang paling banyak memakan waktu prosesor. Pada chapter ini kita menjalankan dua beban kerja berbeda dalam satu jendela perekaman, membandingkan kontribusinya lewat go tool pprof -top, lalu menajamkan hasilnya sampai ke baris kode dengan -list.

Program dengan Dua Beban Kerja #

Berkas contoh.go di bawah halaman menggabungkan dua fungsi berat dengan karakter berbeda: jumlahKuadratMod berisi operasi aritmetika tanpa percabangan, sedangkan hitungPrima menguji pembagian berulang untuk setiap kandidat bilangan prima. Keduanya dijalankan di antara StartCPUProfile dan StopCPUProfile:

go
package main

import (
	"fmt"
	"os"
	"runtime/pprof"
)

// jumlahKuadratMod menjumlahkan (i*i)%101 untuk i dari 0 sampai n-1.
func jumlahKuadratMod(n int) int64 {
	var total int64
	for i := range n {
		x := int64(i)
		total += (x * x) % 101
	}
	return total
}

// hitungPrima menghitung banyaknya bilangan prima di bawah batas
// dengan uji pembagian terhadap setiap kandidat.
func hitungPrima(batas int) int {
	jumlah := 0
	for n := 2; n < batas; n++ {
		prima := true
		for d := 2; d*d <= n; d++ {
			if n%d == 0 {
				prima = false
				break
			}
		}
		if prima {
			jumlah++
		}
	}
	return jumlah
}

func main() {
	berkas, err := os.CreateTemp("", "cpu-*.prof")
	if err != nil {
		fmt.Println("gagal membuat berkas profil:", err)
		return
	}

	if err := pprof.StartCPUProfile(berkas); err != nil {
		fmt.Println("gagal memulai profil CPU:", err)
		return
	}
	hasilKuadrat := jumlahKuadratMod(300_000_000)
	hasilPrima := hitungPrima(1_500_000)
	pprof.StopCPUProfile()
	berkas.Close()

	fmt.Println("jumlah kuadrat mod:", hasilKuadrat)
	fmt.Println("prima di bawah 1500000:", hasilPrima)
	fmt.Println("profil CPU ditulis: true")
}

Output yang Dihasilkan:

text
jumlah kuadrat mod: 14999999855
prima di bawah 1500000: 114155
profil CPU ditulis: true

Mari kita telaah baris demi baris program di atas:

  1. Baris 10–17 (jumlahKuadratMod): loop 300 juta iterasi dengan operasi tetap; hasilnya (14999999855) konstan sehingga beban kerja ini bisa dibandingkan antar eksekusi.
  2. Baris 21–36 (hitungPrima): uji pembagian sampai akar kandidat; loop dalam (baris 25–30) membuat pola akses memori dan percabangan yang jauh lebih padat.
  3. Baris 39–43: berkas profil dibuat lewat os.CreateTemp dengan pola cpu-*.prof; path pastinya berbeda setiap eksekusi dan sengaja tidak dicetak.
  4. Baris 45–52: jendela perekaman dibuka dan ditutup tepat mengelilingi kedua beban kerja — hanya waktu di antara keduanya yang dihitung.
  5. Baris 54–56: hasil kedua fungsi dicetak sebagai bukti beban kerja benar-benar dieksekusi, lalu konfirmasi profil ditulis.

Membandingkan Kontribusi Fungsi #

Setelah program selesai, profilnya siap dibaca. Ganti nama berkas sesuai hasil di mesin Anda, lalu jalankan:

bash
go tool pprof -top /tmp/cpu-XXXX.prof

Output yang Dihasilkan:

text
File: contoh
Build ID: 2817bbb5357bf084c9f1d00c639cbf7d30680ba4
Type: cpu
Duration: 595.15ms, Total samples = 600ms (100.81%)
Showing nodes accounting for 600ms, 100% of 600ms total
      flat  flat%   sum%        cum   cum%
     340ms 56.67% 56.67%      340ms 56.67%  main.hitungPrima (inline)
     260ms 43.33%  100%      260ms 43.33%  main.jumlahKuadratMod (inline)
         0     0%   100%      600ms   100%  main.main
         0     0%   100%      600ms   100%  runtime.main

Tabel ini langsung menjawab pertanyaan pertama setiap optimasi: hitungPrima menyerap 56,67% sampel — hampir 1,3 kali lipat dari jumlahKuadratMod meski batas primanya “hanya” 1,5 juta, karena tiap kandidat prima diuji pembaginya satu per satu. Catatan (inline) menandakan compiler menempelkan fungsi ke pemanggilnya sehingga tidak ada frame terpisah — hal biasa untuk fungsi kecil. Nilai Total samples = 600ms lebih besar dari Duration: 595.15ms karena profil multi-core bisa mencatat lebih dari satu sampel per satuan waktu; itu bukan kesalahan.

Menajamkan ke Baris Kode dengan -list #

Ketika tersangka utama sudah ditentukan, pprof -list <fungsi> menumpahkan sampel ke source code baris demi baris:

bash
go tool pprof -list hitungPrima /tmp/cpu-XXXX.prof

Output yang Dihasilkan:

text
Total: 600ms
ROUTINE ======================== main.hitungPrima in /home/z/my-project/content/go/debugging-dan-profiling/cpu-profiling/contoh.go
     340ms      340ms (flat, cum) 56.67% of Total
         .          .     21:func hitungPrima(batas int) int {
         .          .     22:	jumlah := 0
      50ms       50ms     23:	for n := 2; n < batas; n++ {
         .          .     24:		prima := true
      40ms       40ms     25:		for d := 2; d*d <= n; d++ {
     250ms      250ms     26:			if n%d == 0 {
         .          .     27:				prima = false
         .          .     28:				break
         .          .     29:			}
         .          .     30:		}
         .          .     31:		if prima {

Baris 26 — operasi modulo n%d == 0 — sendirian menyerap 250 ms dari 340 ms milik hitungPrima. Inilah petunjuk konkret: mengganti uji pembagian dengan saringan Eratosthenes akan menghapus operasi modulo yang sama persis. Tanda . berarti baris itu tidak pernah tercatat pada sampel mana pun.

Waktu CPU hanyalah separuh cerita; profil berikutnya menyoroti memori — berapa byte yang dialokasikan dan dari stack mana — pada chapter memory profiling.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

contoh.go

Unduh berkas
go
package main

import (
	"fmt"
	"os"
	"runtime/pprof"
)

// jumlahKuadratMod menjumlahkan (i*i)%101 untuk i dari 0 sampai n-1.
func jumlahKuadratMod(n int) int64 {
	var total int64
	for i := range n {
		x := int64(i)
		total += (x * x) % 101
	}
	return total
}

// hitungPrima menghitung banyaknya bilangan prima di bawah batas
// dengan uji pembagian terhadap setiap kandidat.
func hitungPrima(batas int) int {
	jumlah := 0
	for n := 2; n < batas; n++ {
		prima := true
		for d := 2; d*d <= n; d++ {
			if n%d == 0 {
				prima = false
				break
			}
		}
		if prima {
			jumlah++
		}
	}
	return jumlah
}

func main() {
	berkas, err := os.CreateTemp("", "cpu-*.prof")
	if err != nil {
		fmt.Println("gagal membuat berkas profil:", err)
		return
	}

	if err := pprof.StartCPUProfile(berkas); err != nil {
		fmt.Println("gagal memulai profil CPU:", err)
		return
	}
	hasilKuadrat := jumlahKuadratMod(300_000_000)
	hasilPrima := hitungPrima(1_500_000)
	pprof.StopCPUProfile()
	berkas.Close()

	fmt.Println("jumlah kuadrat mod:", hasilKuadrat)
	fmt.Println("prima di bawah 1500000:", hasilPrima)
	fmt.Println("profil CPU ditulis: true")
}