Kode yang lebih cepat tidak lahir dari firasat — tanpa angka pembanding, kode bisa justru melambat tanpa kita sadari. Go membekali kita dua alat pengukuran bawaan — benchmark pada package testing untuk mengukur satu fungsi secara berulang, dan profiler pprof untuk melihat bagian mana yang memakan waktu. Chapter ini merangkai keduanya dalam satu alur: ukur, profil, ubah, ukur lagi.
Alur Kerja: Ukur, Profil, Ubah, Ukur Lagi #
Alur kerja optimisasi yang sehat berbentuk lingkaran, bukan garis lurus:
- Ukur kode saat ini dengan benchmark untuk mendapat angka dasar (baseline).
- Profil dengan pprof untuk menemukan fungsi yang paling memakan waktu atau paling banyak mengalokasikan memori.
- Ubah satu hal saja pada bagian tersebut.
- Ukur ulang dengan input yang sama, lalu bandingkan dengan baseline.
Kalau angka tidak membaik, kembalikan perubahannya. Donald Knuth — meminjam diksi C. A. R. Hoare — merangkumnya: premature optimization is the root of all evil; mengubah kode sebelum ada bukti pengukuran hanya menambah risiko.
Kode yang Akan Diukur #
Berkas bench-demo.go memuat dua fungsi dengan hasil identik: RingkasanSprintf memformat tiap baris dengan fmt.Sprintf lalu menyambungnya lewat operator +=, sedangkan RingkasanBuilder menampung keluaran di strings.Builder dan menulis angka dengan strconv.AppendInt ke buffer yang dipakai ulang.
package main
import (
"fmt"
"runtime"
"strconv"
"strings"
)
// Transaksi mencatat satu baris penjualan untuk laporan harian.
type Transaksi struct {
ID int
Nama string
Total int
}
// dataUji membangun data tetap berdasarkan angka, sehingga kedua fungsi
// diukur dengan input yang sama dan hasilnya dapat dibandingkan.
func dataUji(n int) []Transaksi {
ts := make([]Transaksi, 0, n)
for i := 1; i <= n; i++ {
ts = append(ts, Transaksi{
ID: i,
Nama: "pesanan-" + strconv.Itoa(i),
Total: i * 15000,
})
}
return ts
}
// RingkasanSprintf memformat laporan memakai fmt.Sprintf per baris.
// Setiap pemanggilan Sprintf dan setiap penyambungan += mengalokasikan
// string baru di heap.
func RingkasanSprintf(ts []Transaksi) string {
laporan := ""
for _, t := range ts {
laporan += fmt.Sprintf("%d|%s|%d\n", t.ID, t.Nama, t.Total)
}
return laporan
}
// RingkasanBuilder memformat laporan yang sama memakai strings.Builder.
// strconv.AppendInt menulis angka ke buffer angka yang dipakai ulang,
// sehingga hampir seluruh baris tidak mengalokasikan memori baru.
func RingkasanBuilder(ts []Transaksi) string {
var b strings.Builder
b.Grow(28 * len(ts)) // perkiraan kapasitas dihitung di muka
var angka [20]byte // buffer sementara di stack
for _, t := range ts {
b.Write(strconv.AppendInt(angka[:0], int64(t.ID), 10))
b.WriteByte('|')
b.WriteString(t.Nama)
b.WriteByte('|')
b.Write(strconv.AppendInt(angka[:0], int64(t.Total), 10))
b.WriteByte('\n')
}
return b.String()
}
func main() {
ts := dataUji(200)
var sebelum, sesudah runtime.MemStats
// Jendela ukur 1: hanya alokasi RingkasanSprintf yang dihitung.
runtime.ReadMemStats(&sebelum)
laporanNaif := RingkasanSprintf(ts)
runtime.ReadMemStats(&sesudah)
alokNaif := sesudah.Mallocs - sebelum.Mallocs
byteNaif := sesudah.TotalAlloc - sebelum.TotalAlloc
// Jendela ukur 2: hanya alokasi RingkasanBuilder yang dihitung.
runtime.ReadMemStats(&sebelum)
laporanBuilder := RingkasanBuilder(ts)
runtime.ReadMemStats(&sesudah)
alokBuilder := sesudah.Mallocs - sebelum.Mallocs
byteBuilder := sesudah.TotalAlloc - sebelum.TotalAlloc
fmt.Println("Kedua versi menghasilkan laporan identik:", laporanNaif == laporanBuilder)
fmt.Println("Jumlah baris laporan:", strings.Count(laporanNaif, "\n"))
fmt.Printf("RingkasanSprintf : %d alokasi, %d byte\n", alokNaif, byteNaif)
fmt.Printf("RingkasanBuilder : %d alokasi, %d byte\n", alokBuilder, byteBuilder)
}Jalankan dari folder chapter:
go run bench-demo.goOutput yang Dihasilkan:
Kedua versi menghasilkan laporan identik: true
Jumlah baris laporan: 200
RingkasanSprintf : 805 alokasi, 479800 byte
RingkasanBuilder : 1 alokasi, 6144 byteMari kita telaah baris demi baris program di atas:
- Baris 17-29 (
func dataUji): membangun 200 baris data tetap; kedua fungsi menerima slice yang sama sehingga pengukurannya adil. - Baris 34-40 (
func RingkasanSprintf): tiap baris memicu dua sumber alokasi — hasilfmt.Sprintfdan hasillaporan +=yang menyalin seluruh isi laporan lama. - Baris 45-58 (
func RingkasanBuilder):b.Growmenyiapkan kapasitas di muka danstrconv.AppendIntmenulis angka ke arrayangkadi stack. - Baris 65-76 (
func main): dua jendela ukur dibuka dan ditutup denganruntime.ReadMemStats; hanya alokasi di antaranya yang dihitung.
Membaca Angka dari runtime.MemStats #
Field Mallocs menghitung jumlah alokasi kumulatif dan TotalAlloc total byte; keduanya naik monoton dan tidak terpengaruh pembersihan GC. Pada eksekusi di atas, RingkasanSprintf melakukan 805 alokasi (479.800 byte) sedangkan RingkasanBuilder hanya 1 alokasi — angka yang sama setiap eksekusi karena alur program tetap.
Benchmark dengan testing.B #
Pengukuran waktu yang adil membutuhkan pengulangan terkendali — tugas framework benchmark Go. Benchmark ditulis sebagai fungsi BenchmarkXxx pada berkas _test.go; sejak Go 1.24 bentuk idiomatiknya memakai for b.Loop() — framework menentukan jumlah iterasi dan hanya kode di dalam loop yang diukur. Dasar penulisannya ada di chapter Benchmark
.
package main
import "testing"
// TestRingkasanKonsisten memastikan kedua implementasi setara sebelum
// performanya dibandingkan: benchmark atas kode yang salah tidak berarti.
func TestRingkasanKonsisten(t *testing.T) {
ts := dataUji(50)
naif := RingkasanSprintf(ts)
cepat := RingkasanBuilder(ts)
if naif != cepat {
t.Fatalf("hasil kedua implementasi berbeda:\n%q\n%q", naif, cepat)
}
}
func BenchmarkRingkasanSprintf(b *testing.B) {
ts := dataUji(200) // setup: tidak ikut diukur oleh for b.Loop()
b.ReportAllocs()
for b.Loop() {
RingkasanSprintf(ts)
}
}
func BenchmarkRingkasanBuilder(b *testing.B) {
ts := dataUji(200) // setup: tidak ikut diukur oleh for b.Loop()
b.ReportAllocs()
for b.Loop() {
RingkasanBuilder(ts)
}
}Poin pentingnya:
- Baris 7-14 (
func TestRingkasanKonsisten): unit test memastikan kedua implementasi setara — kecepatan tidak pernah menutupi kebenaran hasil. - Baris 17 dan 25 (
ts := dataUji(200)): penyiapan data diletakkan sebelumfor b.Loop()sehingga tidak ikut terukur. - Baris 18 dan 26 (
b.ReportAllocs()): memunculkan kolomB/opdanallocs/op; flag-benchmemmengaktifkan kolom yang sama tanpa mengubah kode.
Jalankan benchmark dari folder chapter:
go test -bench=. -benchtime=100xOutput yang Dihasilkan:
goos: linux
goarch: amd64
pkg: tutorial-website/content/go/performance-dan-optimisasi/benchmark-dan-profiling
cpu: Intel(R) Xeon(R) Processor
BenchmarkRingkasanSprintf-2 100 134489 ns/op 479351 B/op 799 allocs/op
BenchmarkRingkasanBuilder-2 100 7991 ns/op 6144 B/op 1 allocs/op
PASS
ok tutorial-website/content/go/performance-dan-optimisasi/benchmark-dan-profiling 0.021sFlag -benchtime=100x membatasi tiap benchmark menjadi 100 iterasi; tanpa flag ini go test -bench memakai 1s bawaan. Pada gaya lama b.N manual, kode setup perlu diikuti b.ResetTimer(); for b.Loop() membuatnya otomatis.
Dari Benchmark ke Profil dengan pprof #
Benchmark membuktikan RingkasanSprintf jauh lebih lambat, tetapi tidak menjelaskan bagian mana yang lambat. Flag -cpuprofile merekam contoh (sample) tumpukan pemanggilan selama benchmark, lalu go tool pprof -top -cum meringkas fungsi dengan bobot terbesar:
go test -run=^$ -bench=BenchmarkRingkasanSprintf -benchtime=5000x -cpuprofile=cpu.out
go tool pprof -top -cum cpu.outOutput yang Dihasilkan (cuplikan):
File: benchmark-dan-profiling.test
Build ID: 6e73b49da34768b3459682f32329db40736f3eda
Type: cpu
Time: 2026-10-13 13:26:49 UTC
Duration: 948.95ms, Total samples = 1.05s (110.65%)
Showing nodes accounting for 1.05s, 100% of 1.05s total
flat flat% sum% cum cum%
0 0% 0% 0.65s 61.90% testing.(*B).run1.func1
0 0% 0% 0.65s 61.90% testing.(*B).runN
0 0% 0% 0.65s 61.90% tutorial-website/content/go/performance-dan-optimisasi/benchmark-dan-profiling.BenchmarkRingkasanSprintf
0 0% 0% 0.65s 61.90% tutorial-website/content/go/performance-dan-optimisasi/benchmark-dan-profiling.RingkasanSprintf
0.01s 0.95% 0.95% 0.40s 38.10% runtime.systemstackMayoritas contoh CPU berada di bawah RingkasanSprintf, dan baris runtime.concatstring2 menyingkap biaya sebenarnya: penyambungan string. Angka bervariasi antar eksekusi — satu eksekusi nyata ditampilkan apa adanya. Bahasan pprof lengkap ada di chapter pprof
.
Micro dan Macro Benchmark #
Benchmark satu fungsi disebut micro benchmark: mudah ditulis dan diulang, tetapi hanya menggambarkan potongan kecil. Macro benchmark mengukur alur utuh — satu request HTTP atau satu pemrosesan berkas — yang lebih dekat ke pengalaman pengguna; keduanya saling melengkapi.
Dengan alat ukur di tangan, kita siap mengeksekusi perubahan pertama yang biasanya paling berdampak: mengurangi jumlah alokasi di heap, topik chapter berikutnya.
Referensi #
- Package testing
— API
testing.B,b.Loop, danb.ReportAllocs. - Package testing — Benchmark
— aturan fungsi
BenchmarkXxx. - pprof untuk program Go — tutorial resmi profiler pprof.
- Diagnostics Go — ringkasan resmi seluruh alat profiling.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
bench-demo_test.go
Unduh berkaspackage main
import "testing"
// TestRingkasanKonsisten memastikan kedua implementasi setara sebelum
// performanya dibandingkan: benchmark atas kode yang salah tidak berarti.
func TestRingkasanKonsisten(t *testing.T) {
ts := dataUji(50)
naif := RingkasanSprintf(ts)
cepat := RingkasanBuilder(ts)
if naif != cepat {
t.Fatalf("hasil kedua implementasi berbeda:\n%q\n%q", naif, cepat)
}
}
func BenchmarkRingkasanSprintf(b *testing.B) {
ts := dataUji(200) // setup: tidak ikut diukur oleh for b.Loop()
b.ReportAllocs()
for b.Loop() {
RingkasanSprintf(ts)
}
}
func BenchmarkRingkasanBuilder(b *testing.B) {
ts := dataUji(200) // setup: tidak ikut diukur oleh for b.Loop()
b.ReportAllocs()
for b.Loop() {
RingkasanBuilder(ts)
}
}
bench-demo.go
Unduh berkaspackage main
import (
"fmt"
"runtime"
"strconv"
"strings"
)
// Transaksi mencatat satu baris penjualan untuk laporan harian.
type Transaksi struct {
ID int
Nama string
Total int
}
// dataUji membangun data tetap berdasarkan angka, sehingga kedua fungsi
// diukur dengan input yang sama dan hasilnya dapat dibandingkan.
func dataUji(n int) []Transaksi {
ts := make([]Transaksi, 0, n)
for i := 1; i <= n; i++ {
ts = append(ts, Transaksi{
ID: i,
Nama: "pesanan-" + strconv.Itoa(i),
Total: i * 15000,
})
}
return ts
}
// RingkasanSprintf memformat laporan memakai fmt.Sprintf per baris.
// Setiap pemanggilan Sprintf dan setiap penyambungan += mengalokasikan
// string baru di heap.
func RingkasanSprintf(ts []Transaksi) string {
laporan := ""
for _, t := range ts {
laporan += fmt.Sprintf("%d|%s|%d\n", t.ID, t.Nama, t.Total)
}
return laporan
}
// RingkasanBuilder memformat laporan yang sama memakai strings.Builder.
// strconv.AppendInt menulis angka ke buffer angka yang dipakai ulang,
// sehingga hampir seluruh baris tidak mengalokasikan memori baru.
func RingkasanBuilder(ts []Transaksi) string {
var b strings.Builder
b.Grow(28 * len(ts)) // perkiraan kapasitas dihitung di muka
var angka [20]byte // buffer sementara di stack
for _, t := range ts {
b.Write(strconv.AppendInt(angka[:0], int64(t.ID), 10))
b.WriteByte('|')
b.WriteString(t.Nama)
b.WriteByte('|')
b.Write(strconv.AppendInt(angka[:0], int64(t.Total), 10))
b.WriteByte('\n')
}
return b.String()
}
func main() {
ts := dataUji(200)
var sebelum, sesudah runtime.MemStats
// Jendela ukur 1: hanya alokasi RingkasanSprintf yang dihitung.
runtime.ReadMemStats(&sebelum)
laporanNaif := RingkasanSprintf(ts)
runtime.ReadMemStats(&sesudah)
alokNaif := sesudah.Mallocs - sebelum.Mallocs
byteNaif := sesudah.TotalAlloc - sebelum.TotalAlloc
// Jendela ukur 2: hanya alokasi RingkasanBuilder yang dihitung.
runtime.ReadMemStats(&sebelum)
laporanBuilder := RingkasanBuilder(ts)
runtime.ReadMemStats(&sesudah)
alokBuilder := sesudah.Mallocs - sebelum.Mallocs
byteBuilder := sesudah.TotalAlloc - sebelum.TotalAlloc
fmt.Println("Kedua versi menghasilkan laporan identik:", laporanNaif == laporanBuilder)
fmt.Println("Jumlah baris laporan:", strings.Count(laporanNaif, "\n"))
fmt.Printf("RingkasanSprintf : %d alokasi, %d byte\n", alokNaif, byteNaif)
fmt.Printf("RingkasanBuilder : %d alokasi, %d byte\n", alokBuilder, byteBuilder)
}