Slice dan map adalah dua struktur data yang paling sering kita pakai, dan keduanya punya biaya tersembunyi: pertumbuhan kapasitas yang menyalin data, rehash map yang membangun bucket baru, serta salinan struct besar saat iterasi. Kabar baiknya, kebiasaan yang mengatasinya hampir selalu murah — satu argumen kapasitas di make, satu perubahan pola loop. Chapter ini mengukur dampak tiap kebiasaan dengan runtime.ReadMemStats lalu menutup dengan panduan memilih antara slice terurut dan map untuk pencarian.

Lima Pola dalam Satu Program #

Berkas slice-demo.go mengukur lima pola: pertumbuhan append tanpa kapasitas, prealokasi dengan make, filter in-place, pengisian map dengan dan tanpa size hint, binary search, serta iterasi struct besar lewat indeks.

go
package main

import (
	"fmt"
	"runtime"
	"slices"
	"strconv"
)

// BarisPesanan struct besar: salinan per iterasi range mahal secara CPU
// karena runtime menyalin seluruh 280 byte pada tiap putaran.
type BarisPesanan struct {
	ID         int64
	Pembeli    [8]string
	Jumlah     [8]int
	Alamat     [4]string
	Keterangan string
}

var sink []int

// ukurMallocs menjalankan f di antara dua pembacaan MemStats dan
// mengembalikan jumlah alokasi yang terjadi di dalam f.
func ukurMallocs(f func()) int64 {
	var sebelum, sesudah runtime.MemStats
	runtime.ReadMemStats(&sebelum)
	f()
	runtime.ReadMemStats(&sesudah)
	return int64(sesudah.Mallocs - sebelum.Mallocs)
}

// filterInPlace menyaring elemen tanpa alokasi baru: backing array lama
// dipakai ulang untuk menampung hasil, isi asal tertimpa.
func filterInPlace(asal []int) []int {
	hasil := asal[:0]
	for _, v := range asal {
		if v%3 != 0 {
			hasil = append(hasil, v)
		}
	}
	return hasil
}

func main() {
	jumlah := 1000

	// Pola 1: append tanpa kapasitas menumbuhkan backing array berulang kali.
	tanpaKapasitas := ukurMallocs(func() {
		var xs []int
		for i := 0; i < jumlah; i++ {
			xs = append(xs, i)
		}
		sink = xs
	})
	denganKapasitas := ukurMallocs(func() {
		xs := make([]int, 0, jumlah)
		for i := 0; i < jumlah; i++ {
			xs = append(xs, i)
		}
		sink = xs
	})
	fmt.Printf("append %d elemen tanpa kapasitas : %d alokasi\n", jumlah, tanpaKapasitas)
	fmt.Printf("append %d elemen dengan make(cap): %d alokasi\n", jumlah, denganKapasitas)

	// Pola 2: filter in-place memakai ulang backing array: nol alokasi.
	asal := make([]int, 0, jumlah)
	for i := 0; i < jumlah; i++ {
		asal = append(asal, i)
	}
	hasilFilter := ukurMallocs(func() {
		asal = filterInPlace(asal)
	})
	fmt.Printf("filter in-place %d elemen        : %d alokasi, %d elemen tersisa\n",
		jumlah, hasilFilter, len(asal))

	// Pola 3: ukuran map yang diketahui di muka menghemat rehash.
	kunci := buatKunci(500)
	tanpaHint := ukurMallocs(func() {
		m := make(map[string]int)
		for i, k := range kunci {
			m[k] = i
		}
	})
	denganHint := ukurMallocs(func() {
		m := make(map[string]int, len(kunci))
		for i, k := range kunci {
			m[k] = i
		}
	})
	fmt.Printf("map 500 kunci tanpa hint        : %d alokasi\n", tanpaHint)
	fmt.Printf("map 500 kunci dengan hint       : %d alokasi\n", denganHint)

	// Pola 4: binary search pada slice terurut, tanpa alokasi.
	terurut := []int{10, 20, 30, 40, 50, 60}
	posisi, ketemu := slices.BinarySearch(terurut, 40)
	fmt.Println("BinarySearch 40 pada 6 elemen   : indeks", posisi, "ditemukan", ketemu)

	// Pola 5: range menyalin struct besar; akses lewat indeks menghindari salinan.
	baris := buatBaris()
	total := 0
	for i := range baris { // tanpa salinan BarisPesanan per iterasi
		if baris[i].ID != 0 {
			total++
		}
	}
	fmt.Println("Baris terisi lewat indeks       :", total, "dari", len(baris))
}

func buatKunci(n int) []string {
	ks := make([]string, 0, n)
	for i := 0; i < n; i++ {
		ks = append(ks, "kunci-"+strconv.Itoa(i))
	}
	return ks
}

func buatBaris() []BarisPesanan {
	b := make([]BarisPesanan, 3)
	b[0].ID = 101
	b[1].ID = 102
	b[2].ID = 103
	return b
}

Jalankan dari folder chapter:

bash
go run slice-demo.go

Output yang Dihasilkan:

text
append 1000 elemen tanpa kapasitas : 9 alokasi
append 1000 elemen dengan make(cap): 1 alokasi
filter in-place 1000 elemen        : 0 alokasi, 666 elemen tersisa
map 500 kunci tanpa hint        : 15 alokasi
map 500 kunci dengan hint       : 3 alokasi
BinarySearch 40 pada 6 elemen   : indeks 3 ditemukan true
Baris terisi lewat indeks       : 3 dari 3

Mari kita telaah baris demi baris program di atas:

  1. Baris 24-30 (func ukurMallocs): jendela ukur alokasi yang sama dengan chapter sebelumnya, dibungkus jadi helper.
  2. Baris 34-42 (func filterInPlace): hasil := asal[:0] memakai ulang backing array milik asal — panjangnya nol, kapasitasnya penuh.
  3. Baris 48-61 (pola 1): dua loop append yang identik; bedanya hanya make([]int, 0, jumlah) di versi kedua.
  4. Baris 70-72 (pola 2): hasil filterInPlace menimpa variabel asal — nol alokasi, 666 dari 1000 elemen tersisa.
  5. Baris 78-89 (pola 3): dua map diisi kunci yang sama; hanya versi kedua yang memberi hint ukuran lewat make(map[string]int, len(kunci)).
  6. Baris 101-105 (pola 5): for i := range baris hanya menyalin indeks, bukan seluruh struct BarisPesanan yang berukuran 280 byte.

Prealokasi Kapasitas dan Pertumbuhan Slice #

append tidak bisa tahu berapa banyak elemen yang akan datang, jadi runtime bermain aman: kapasitas digandakan bertahap (4 → 8 → 16 → … → 1024) dan setiap penggandaan mengalokasikan backing array baru lalu menyalin seluruh isi lama ke sana. Untuk 1000 elemen, proses itu menghasilkan 9 alokasi dan 9 kali penyalinan; dengan make([]int, 0, 1000), semuanya cukup satu alokasi tanpa salinan.

Filter in-place: Nol Alokasi #

Pola filter biasa membuat slice hasil baru, padahal backing array asal masih sanggup menampung. hasil := asal[:0] memakai ulang memori itu: append menulis ke posisi yang sudah ada tanpa alokasi, terbukti dari angka nol pada output. Pola ini cocok ketika data asal tidak lagi dibutuhkan setelah penyaringan.

Size Hint untuk Map #

Map membagi datanya ke dalam bucket. Ketika bucket mulai penuh, runtime mengalokasikan bucket baru dua kali lipat besar lalu memindahkan seluruh isi — proses rehash. Mengisi 500 kunci tanpa hint menghasilkan 15 alokasi dari rangkaian rehash tersebut; dengan make(map[string]int, 500), runtime menyiapkan cukup bucket sekali saja (3 alokasi) dan rehash tidak pernah terjadi.

Kunci, Salinan Struct, dan Cache Locality #

Tiga kebiasaan tambahan menjaga slice dan map tetap cepat. Pertama, kunci int lebih murah di-hash daripada string yang harus diproses byte demi byte; kalau domain masalah mengizinkan angka, gunakan angka sebagai kunci. Kedua, []T dengan T berupa struct membuat data berdampingan di memori (cache locality) — CPU membaca elemen berikutnya dari cache; []*T memaksa CPU melompat-lompat mengikuti pointer. Ketiga, for _, v := range baris menyalin v pada setiap iterasi — untuk struct 280 byte, 280 byte tersalin berulang; akses lewat baris[i] menghindarinya. Konversi []byte menjadi kunci lookup m[string(b)] juga dihindari compiler saat dipakai langsung dalam ekspresi indeks map.

Binary Search atau Map Lookup #

Untuk pencarian berulang, slice terurut dan map punya keunggulan yang berbeda:

AspekSlice terurut + BinarySearchMap
Kompleksitas pencarianO(log n)O(1) rata-rata
Alokasi saat lookupNolNol
Memori tambahanNol (data itu sendiri)Bucket + pointer per kunci
SyaratData terurutBebas
Cocok untukData kecil, lookup jarang, hemat memoriLookup sangat sering

sort.Search dan slices.BinarySearch membagi dua rentang pencarian berulang kali tanpa mengalokasikan apa pun. Pada koleksi puluhan elemen dengan lookup tidak terlalu sering, slice terurut sering menang karena datanya rapat di cache; ketika lookup berjuta-juta kali, map unggul.

Dengan slice dan map yang rapi, sumber alokasi terbesar yang tersisa ada di pengolahan teks: penyambungan string di dalam loop — chapter berikutnya membongkarnya dengan strings.Builder.

Referensi #

Berkas Sumber

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

slice-demo.go

Unduh berkas
go
package main

import (
	"fmt"
	"runtime"
	"slices"
	"strconv"
)

// BarisPesanan struct besar: salinan per iterasi range mahal secara CPU
// karena runtime menyalin seluruh 280 byte pada tiap putaran.
type BarisPesanan struct {
	ID         int64
	Pembeli    [8]string
	Jumlah     [8]int
	Alamat     [4]string
	Keterangan string
}

var sink []int

// ukurMallocs menjalankan f di antara dua pembacaan MemStats dan
// mengembalikan jumlah alokasi yang terjadi di dalam f.
func ukurMallocs(f func()) int64 {
	var sebelum, sesudah runtime.MemStats
	runtime.ReadMemStats(&sebelum)
	f()
	runtime.ReadMemStats(&sesudah)
	return int64(sesudah.Mallocs - sebelum.Mallocs)
}

// filterInPlace menyaring elemen tanpa alokasi baru: backing array lama
// dipakai ulang untuk menampung hasil, isi asal tertimpa.
func filterInPlace(asal []int) []int {
	hasil := asal[:0]
	for _, v := range asal {
		if v%3 != 0 {
			hasil = append(hasil, v)
		}
	}
	return hasil
}

func main() {
	jumlah := 1000

	// Pola 1: append tanpa kapasitas menumbuhkan backing array berulang kali.
	tanpaKapasitas := ukurMallocs(func() {
		var xs []int
		for i := 0; i < jumlah; i++ {
			xs = append(xs, i)
		}
		sink = xs
	})
	denganKapasitas := ukurMallocs(func() {
		xs := make([]int, 0, jumlah)
		for i := 0; i < jumlah; i++ {
			xs = append(xs, i)
		}
		sink = xs
	})
	fmt.Printf("append %d elemen tanpa kapasitas : %d alokasi\n", jumlah, tanpaKapasitas)
	fmt.Printf("append %d elemen dengan make(cap): %d alokasi\n", jumlah, denganKapasitas)

	// Pola 2: filter in-place memakai ulang backing array: nol alokasi.
	asal := make([]int, 0, jumlah)
	for i := 0; i < jumlah; i++ {
		asal = append(asal, i)
	}
	hasilFilter := ukurMallocs(func() {
		asal = filterInPlace(asal)
	})
	fmt.Printf("filter in-place %d elemen        : %d alokasi, %d elemen tersisa\n",
		jumlah, hasilFilter, len(asal))

	// Pola 3: ukuran map yang diketahui di muka menghemat rehash.
	kunci := buatKunci(500)
	tanpaHint := ukurMallocs(func() {
		m := make(map[string]int)
		for i, k := range kunci {
			m[k] = i
		}
	})
	denganHint := ukurMallocs(func() {
		m := make(map[string]int, len(kunci))
		for i, k := range kunci {
			m[k] = i
		}
	})
	fmt.Printf("map 500 kunci tanpa hint        : %d alokasi\n", tanpaHint)
	fmt.Printf("map 500 kunci dengan hint       : %d alokasi\n", denganHint)

	// Pola 4: binary search pada slice terurut, tanpa alokasi.
	terurut := []int{10, 20, 30, 40, 50, 60}
	posisi, ketemu := slices.BinarySearch(terurut, 40)
	fmt.Println("BinarySearch 40 pada 6 elemen   : indeks", posisi, "ditemukan", ketemu)

	// Pola 5: range menyalin struct besar; akses lewat indeks menghindari salinan.
	baris := buatBaris()
	total := 0
	for i := range baris { // tanpa salinan BarisPesanan per iterasi
		if baris[i].ID != 0 {
			total++
		}
	}
	fmt.Println("Baris terisi lewat indeks       :", total, "dari", len(baris))
}

func buatKunci(n int) []string {
	ks := make([]string, 0, n)
	for i := 0; i < n; i++ {
		ks = append(ks, "kunci-"+strconv.Itoa(i))
	}
	return ks
}

func buatBaris() []BarisPesanan {
	b := make([]BarisPesanan, 3)
	b[0].ID = 101
	b[1].ID = 102
	b[2].ID = 103
	return b
}