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:

  1. Ukur kode saat ini dengan benchmark untuk mendapat angka dasar (baseline).
  2. Profil dengan pprof untuk menemukan fungsi yang paling memakan waktu atau paling banyak mengalokasikan memori.
  3. Ubah satu hal saja pada bagian tersebut.
  4. 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.

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

bash
go run bench-demo.go

Output yang Dihasilkan:

text
Kedua versi menghasilkan laporan identik: true
Jumlah baris laporan: 200
RingkasanSprintf : 805 alokasi, 479800 byte
RingkasanBuilder : 1 alokasi, 6144 byte

Mari kita telaah baris demi baris program di atas:

  1. Baris 17-29 (func dataUji): membangun 200 baris data tetap; kedua fungsi menerima slice yang sama sehingga pengukurannya adil.
  2. Baris 34-40 (func RingkasanSprintf): tiap baris memicu dua sumber alokasi — hasil fmt.Sprintf dan hasil laporan += yang menyalin seluruh isi laporan lama.
  3. Baris 45-58 (func RingkasanBuilder): b.Grow menyiapkan kapasitas di muka dan strconv.AppendInt menulis angka ke array angka di stack.
  4. Baris 65-76 (func main): dua jendela ukur dibuka dan ditutup dengan runtime.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 .

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

  1. Baris 7-14 (func TestRingkasanKonsisten): unit test memastikan kedua implementasi setara — kecepatan tidak pernah menutupi kebenaran hasil.
  2. Baris 17 dan 25 (ts := dataUji(200)): penyiapan data diletakkan sebelum for b.Loop() sehingga tidak ikut terukur.
  3. Baris 18 dan 26 (b.ReportAllocs()): memunculkan kolom B/op dan allocs/op; flag -benchmem mengaktifkan kolom yang sama tanpa mengubah kode.

Jalankan benchmark dari folder chapter:

bash
go test -bench=. -benchtime=100x

Output yang Dihasilkan:

text
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.021s

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

bash
go test -run=^$ -bench=BenchmarkRingkasanSprintf -benchtime=5000x -cpuprofile=cpu.out
go tool pprof -top -cum cpu.out

Output yang Dihasilkan (cuplikan):

text
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.systemstack

Mayoritas 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 #

Berkas Sumber

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

bench-demo_test.go

Unduh berkas
go
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)
	}
}

bench-demo.go

Unduh berkas
go
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)
}