Kode yang kita tulis bukan kode yang dieksekusi: compiler Go menyisipkan optimisasi diam-diam di antara keduanya. Fungsi kecil digabung ke pemanggilnya, pemeriksaan batas slice yang terbukti aman dihapus, dan panggilan interface yang tipenya sudah diketahui diubah menjadi panggilan langsung. Memahami optimisasi ini menjaga kita dari dua kesalahan berlawanan — mengira pemecahan fungsi kecil selalu mahal, atau mengira unsafe diperlukan demi kecepatan. Semua buktinya dapat dibaca langsung dari laporan compiler lewat flag -gcflags=-m.

Program: Fungsi Kecil, Fungsi Berat, dan Interface #

Berkas compiler-demo.go memuat bahan uji lengkap: fungsi kecil kuadrat, fungsi berat prosesBerat, tiga pola akses slice yang berbeda kekuatan buktinya, dan satu panggilan lewat interface yang tipenya statis.

go
package main

import "fmt"

// kuadrat kecil dan sederhana: kandidat kuat inlining, panggilannya
// diganti salinan isi fungsi sehingga bebas biaya pemanggilan.
func kuadrat(x int) int {
	return x * x
}

// prosesBerat terlalu besar untuk anggaran inlining compiler, sehingga
// setiap pemanggilan tetap menjadi instruksi call sungguhan.
func prosesBerat(xs []int) int {
	akumulator := 0
	for i := 0; i < len(xs); i++ {
		v := xs[i]
		if v > 0 {
			akumulator += v * v
		} else if v < 0 {
			akumulator -= v
		} else {
			akumulator += 1
		}
		switch v % 4 {
		case 0:
			akumulator += 2
		case 1:
			akumulator += 3
		case 2:
			akumulator += 5
		default:
			akumulator += 7
		}
		if akumulator > 1_000_000 {
			akumulator -= 1_000_000
		}
		if akumulator < -1_000_000 {
			akumulator += 1_000_000
		}
	}
	return akumulator
}

// jumlahDenganIndeks memakai pola i < len(xs): compiler membuktikan
// indeks selalu valid dan menghilangkan bounds check di dalam loop.
func jumlahDenganIndeks(xs []int) int {
	total := 0
	for i := 0; i < len(xs); i++ {
		total += xs[i]
	}
	return total
}

// ambilAman memeriksa batas secara eksplisit sebelum mengakses elemen.
func ambilAman(xs []int, i int) int {
	if i < 0 || i >= len(xs) {
		return 0
	}
	return xs[i]
}

// ambilLangsung mengakses elemen tanpa bukti kevalidan indeks: compiler
// tidak dapat menghilangkan bounds check pada fungsi ini.
func ambilLangsung(xs []int, i int) int {
	return xs[i]
}

// Bentuk interface dengan satu method; Persegi adalah implementasinya.
type Bentuk interface {
	Luas() int
}

type Persegi struct {
	sisi int
}

func (p Persegi) Luas() int { return p.sisi * p.sisi }

func main() {
	angka := []int{1, 2, 3, 4, 5, 6, 7, 8}
	fmt.Println("kuadrat(12)            :", kuadrat(12))
	fmt.Println("prosesBerat(angka)     :", prosesBerat(angka))
	fmt.Println("jumlahDenganIndeks     :", jumlahDenganIndeks(angka))
	fmt.Println("ambilAman(angka, 3)    :", ambilAman(angka, 3))
	fmt.Println("ambilAman(angka, 9)    :", ambilAman(angka, 9))
	fmt.Println("ambilLangsung(angka, 2):", ambilLangsung(angka, 2))
	p := Persegi{sisi: 7}
	var b Bentuk = p
	fmt.Println("Luas lewat interface   :", b.Luas())
}

Jalankan dari folder chapter:

bash
go run compiler-demo.go

Output yang Dihasilkan:

text
kuadrat(12)            : 144
prosesBerat(angka)     : 238
jumlahDenganIndeks     : 36
ambilAman(angka, 3)    : 4
ambilAman(angka, 9)    : 0
ambilLangsung(angka, 2): 3
Luas lewat interface   : 49
  1. Baris 7-9 (func kuadrat): satu ekspresi — kandidat inlining paling kuat.
  2. Baris 13-42 (func prosesBerat): loop bercabang yang melebihi anggaran inlining.
  3. Baris 46-52 dan 55-60: pola loop i < len(xs) dan pemeriksaan eksplisit keduanya menjadi bukti kevalidan indeks.
  4. Baris 64-66 (func ambilLangsung): parameter i bebas — bukti tidak tersedia.
  5. Baris 87-89: var b Bentuk = p lalu b.Luas() — tipe konkretnya diketahui compiler di titik panggilan.

Inlining: Panggilan yang Hilang #

Pemanggilan fungsi menyimpan register, melompat ke alamat baru, lalu kembali — biaya kecil yang menumpuk pada fungsi yang dipanggil jutaan kali. Inlining menghapus lompatan tersebut: isi fungsi kecil disalin langsung ke lokasi pemanggilannya. Compiler menilai kandidat lewat anggaran biaya; laporan berikut menunjukkan kuadrat lolos (biaya 4) sementara prosesBerat ditolak (biaya 83 melebihi anggaran 80):

bash
go build -gcflags=-m=2 compiler-demo.go 2>&1 | grep "^\./compiler-demo" | grep -E "inlin|devirt" | grep -v Println

Output yang Dihasilkan:

text
./compiler-demo.go:7:6: can inline kuadrat with cost 4 as: func(int) int { return x * x }
./compiler-demo.go:13:6: cannot inline prosesBerat: function too complex: cost 83 exceeds budget 80
./compiler-demo.go:46:6: can inline jumlahDenganIndeks with cost 25 as: func([]int) int { total := 0; for loop; return total }
./compiler-demo.go:55:6: can inline ambilAman with cost 15 as: func([]int, int) int { if i < 0 || i >= len(xs) { return 0 }; return xs[i] }
./compiler-demo.go:64:6: can inline ambilLangsung with cost 4 as: func([]int, int) int { return xs[i] }
./compiler-demo.go:77:6: can inline Persegi.Luas with cost 6 as: method(Persegi) func() int { return p.sisi * p.sisi }
./compiler-demo.go:79:6: cannot inline main: function too complex: cost 781 exceeds budget 80
./compiler-demo.go:89:48: devirtualizing b.Luas to Persegi
./compiler-demo.go:81:49: inlining call to kuadrat
./compiler-demo.go:83:60: inlining call to jumlahDenganIndeks
./compiler-demo.go:84:51: inlining call to ambilAman
./compiler-demo.go:85:51: inlining call to ambilAman
./compiler-demo.go:86:55: inlining call to ambilLangsung
./compiler-demo.go:89:48: inlining call to Persegi.Luas

Baris can inline kuadrat menyatakan kelayakan, dan baris inlining call to kuadrat membuktikan panggilan kuadrat(12) di main benar-benar diganti perkalian langsung. Praktisnya: fungsi helper kecil dan pendek — getter, pembungkus satu baris — tidak perlu digabung manual ke pemanggil demi kecepatan; biarkan compiler yang bekerja.

Bounds Check Elimination #

Setiap xs[i] secara bawaan diperiksa: bila i di luar rentang, runtime memicu panic. Pemeriksaan itu memakan beberapa instruksi tiap akses, tetapi banyak di antaranya dapat dibuktikan tidak perlu. Loop for i := 0; i < len(xs); i++ menjamin i selalu valid sehingga pemeriksaan di dalamnya dihapus. Diagnostics -d=ssa/check_bce mencatat akses mana yang tetap membawa pemeriksaan:

bash
go build -gcflags="-d=ssa/check_bce" compiler-demo.go 2>&1

Output yang Dihasilkan:

text

Hanya ambilLangsung yang muncul: parameternya bebas sehingga compiler tidak bisa membuktikan validitas indeks. jumlahDenganIndeks dan ambilAman tidak muncul karena pola loop dan pemeriksaan eksplisit mereka sudah menjadi bukti yang cukup.

Devirtualization #

Panggilan b.Luas() lewat interface secara bawaan dua langkah: cari method di tabel method tipe konkret (itab), lalu panggil alamatnya — satu lompatan tidak langsung yang sulit diprediksi CPU. Ketika compiler dapat membuktikan tipe konkretnya (di sini var b Bentuk = p dengan p bertipe Persegi), panggilan di-devirtualisasi menjadi panggilan langsung. Rantai di laporan pertama menunjukkan hasilnya: devirtualizing b.Luas to Persegi diikuti inlining call to Persegi.Luas — setelah menjadi panggilan langsung, methodnya pun ikut di-inline. Pembuktian paling kuat terjadi saat nilai interface dibuat dan dipakai dalam satu fungsi; interface yang datang dari parameter tetap menjadi panggilan tidak langsung.

Flag untuk Debug dan Optimisasi Lanjutan #

Empat hal terakhir melengkapi gambar compiler:

  • -gcflags="-N -l": menonaktifkan optimisasi dan inlining untuk binary debug — breakpoint dan langkah debugger menempel pada kode yang ditulis, bukan hasil transformasi.
  • PGO (Go 1.21+): profile-guided optimization — profil produksi pprof diumpankan ke compiler agar cabang panas dan kandidat inlining diprioritaskan, aktif lewat berkas default.pgo di akar package.
  • Pertumbuhan stack: goroutine memulai dengan stack kecil lalu tumbuh dengan menyalin seluruh isi ke blok lebih besar saat dibutuhkan — murah, dan alasan rekursi dalam tidak perlu dihindari karena mitos “stack terbatas”.
  • Escape analysis: keputusan stack/heap yang membentuk laporan -m — dibahas tuntas pada chapter Escape Analysis .

Optimisasi compiler bekerja otomatis, tetapi keputusan di mana berusaha tetap milik kita. Chapter penutup section ini merangkum disiplinnya: ukur dulu, ubah kemudian — dan jangan percaya dugaan sebelum angka membuktikannya.

Referensi #

Berkas Sumber

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

compiler-demo.go

Unduh berkas
go
package main

import "fmt"

// kuadrat kecil dan sederhana: kandidat kuat inlining, panggilannya
// diganti salinan isi fungsi sehingga bebas biaya pemanggilan.
func kuadrat(x int) int {
	return x * x
}

// prosesBerat terlalu besar untuk anggaran inlining compiler, sehingga
// setiap pemanggilan tetap menjadi instruksi call sungguhan.
func prosesBerat(xs []int) int {
	akumulator := 0
	for i := 0; i < len(xs); i++ {
		v := xs[i]
		if v > 0 {
			akumulator += v * v
		} else if v < 0 {
			akumulator -= v
		} else {
			akumulator += 1
		}
		switch v % 4 {
		case 0:
			akumulator += 2
		case 1:
			akumulator += 3
		case 2:
			akumulator += 5
		default:
			akumulator += 7
		}
		if akumulator > 1_000_000 {
			akumulator -= 1_000_000
		}
		if akumulator < -1_000_000 {
			akumulator += 1_000_000
		}
	}
	return akumulator
}

// jumlahDenganIndeks memakai pola i < len(xs): compiler membuktikan
// indeks selalu valid dan menghilangkan bounds check di dalam loop.
func jumlahDenganIndeks(xs []int) int {
	total := 0
	for i := 0; i < len(xs); i++ {
		total += xs[i]
	}
	return total
}

// ambilAman memeriksa batas secara eksplisit sebelum mengakses elemen.
func ambilAman(xs []int, i int) int {
	if i < 0 || i >= len(xs) {
		return 0
	}
	return xs[i]
}

// ambilLangsung mengakses elemen tanpa bukti kevalidan indeks: compiler
// tidak dapat menghilangkan bounds check pada fungsi ini.
func ambilLangsung(xs []int, i int) int {
	return xs[i]
}

// Bentuk interface dengan satu method; Persegi adalah implementasinya.
type Bentuk interface {
	Luas() int
}

type Persegi struct {
	sisi int
}

func (p Persegi) Luas() int { return p.sisi * p.sisi }

func main() {
	angka := []int{1, 2, 3, 4, 5, 6, 7, 8}
	fmt.Println("kuadrat(12)            :", kuadrat(12))
	fmt.Println("prosesBerat(angka)     :", prosesBerat(angka))
	fmt.Println("jumlahDenganIndeks     :", jumlahDenganIndeks(angka))
	fmt.Println("ambilAman(angka, 3)    :", ambilAman(angka, 3))
	fmt.Println("ambilAman(angka, 9)    :", ambilAman(angka, 9))
	fmt.Println("ambilLangsung(angka, 2):", ambilLangsung(angka, 2))
	p := Persegi{sisi: 7}
	var b Bentuk = p
	fmt.Println("Luas lewat interface   :", b.Luas())
}