Thursday

19-06-2025 Vol 19

For anyone new to benchmarking in Go

Benchmarking di Go untuk Pemula: Panduan Lengkap

Benchmarking adalah proses menjalankan kode Anda berulang kali untuk mengukur kinerjanya. Dalam Go, benchmarking sangat penting untuk mengidentifikasi bottleneck, mengoptimalkan kode, dan memastikan bahwa perubahan yang Anda buat benar-benar meningkatkan kinerja. Panduan ini dirancang untuk pemula yang ingin memahami dan menggunakan benchmarking dalam proyek Go mereka.

Mengapa Benchmarking Penting di Go?

Benchmarking memberikan wawasan yang berharga tentang kinerja kode Anda. Berikut adalah beberapa alasan mengapa benchmarking penting:

  1. Optimasi Kinerja: Membantu mengidentifikasi bagian-bagian kode yang lambat dan perlu dioptimalkan.
  2. Perbandingan Implementasi: Memungkinkan Anda membandingkan kinerja berbagai pendekatan algoritmik atau implementasi.
  3. Deteksi Regresi: Memastikan perubahan kode baru tidak menurunkan kinerja yang ada.
  4. Pengambilan Keputusan yang Terinformasi: Memberikan data empiris untuk mendukung keputusan tentang arsitektur dan desain.

Dasar-Dasar Benchmarking di Go

Go menyediakan dukungan bawaan untuk benchmarking melalui paket testing. Benchmark ditulis sebagai fungsi yang mirip dengan pengujian unit, tetapi mereka dijalankan oleh alat go test dengan flag khusus.

Struktur Dasar Benchmark

Sebuah benchmark Go didefinisikan sebagai fungsi yang mengikuti pola berikut:


func BenchmarkFunctionName(b *testing.B) {
    // Kode yang akan di-benchmark
}
  • BenchmarkFunctionName: Nama fungsi benchmark harus dimulai dengan Benchmark.
  • b *testing.B: Parameter b adalah pointer ke struct testing.B, yang menyediakan metode untuk mengontrol dan mengukur kinerja benchmark.

Contoh Benchmark Sederhana

Mari kita lihat contoh benchmark sederhana:


package main

import (
	"testing"
)

func BenchmarkSimpleLoop(b *testing.B) {
	for i := 0; i < b.N; i++ {
		// Kode yang akan di-benchmark
		_ = i * 2
	}
}

Dalam contoh ini, loop dijalankan b.N kali. b.N adalah jumlah iterasi yang ditentukan secara dinamis oleh alat go test untuk mendapatkan hasil yang akurat dan stabil.

Menjalankan Benchmark

Untuk menjalankan benchmark, gunakan perintah go test dengan flag -bench. Flag ini diikuti oleh regular expression yang menentukan benchmark mana yang akan dijalankan. Untuk menjalankan semua benchmark, gunakan . sebagai regular expression.

go test -bench=.

Memahami Output Benchmark

Output benchmark akan terlihat seperti ini:


goos: darwin
goarch: amd64
pkg: your_package
BenchmarkSimpleLoop-8   	68347253	        17.4 ns/op
PASS
ok  	your_package	1.201s

Mari kita uraikan output ini:

  • goos: Sistem operasi (misalnya, darwin).
  • goarch: Arsitektur (misalnya, amd64).
  • pkg: Nama paket.
  • BenchmarkSimpleLoop-8: Nama benchmark dan jumlah CPU yang digunakan (GOMAXPROCS).
  • 68347253: Jumlah iterasi yang dijalankan benchmark.
  • 17.4 ns/op: Waktu rata-rata yang dibutuhkan per operasi (nanodetik per operasi).
  • PASS: Menunjukkan bahwa benchmark berhasil.
  • ok your_package 1.201s: Menunjukkan bahwa pengujian dan benchmark di paket tersebut berhasil dan total waktu yang dibutuhkan.

Contoh yang Lebih Kompleks

Mari kita buat contoh benchmark yang lebih kompleks yang melibatkan alokasi memori:


package main

import (
	"testing"
)

func BenchmarkStringConcat(b *testing.B) {
	for i := 0; i < b.N; i++ {
		_ = "hello" + "world"
	}
}

func BenchmarkStringBuilder(b *testing.B) {
	for i := 0; i < b.N; i++ {
		var builder strings.Builder
		builder.WriteString("hello")
		builder.WriteString("world")
		_ = builder.String()
	}
}

Di sini kita membandingkan dua cara untuk menggabungkan string: menggunakan operator + dan menggunakan strings.Builder.

Jalankan benchmark:

go test -bench=.

Output yang mungkin:


goos: darwin
goarch: amd64
pkg: your_package
BenchmarkStringConcat-8   	11564309	       104 ns/op
BenchmarkStringBuilder-8  	 5254090	       227 ns/op
PASS
ok  	your_package	2.867s

Dalam contoh ini, kita dapat melihat bahwa menggabungkan string dengan operator + lebih cepat daripada menggunakan strings.Builder dalam kasus ini. Ini mungkin mengejutkan, tetapi menyoroti pentingnya benchmarking sebelum mengoptimalkan kode secara prematur.

Fitur Lanjutan Benchmarking

Paket testing menyediakan beberapa fitur lanjutan untuk benchmarking yang lebih akurat dan kontrol yang lebih baik.

b.ResetTimer()

b.ResetTimer() menghentikan timer benchmark. Ini berguna untuk mengatur operasi yang tidak boleh diukur sebagai bagian dari benchmark (misalnya, inisialisasi).


func BenchmarkWithResetTimer(b *testing.B) {
	// Inisialisasi yang mahal
	data := prepareData()

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		// Kode yang akan di-benchmark
		processData(data)
	}
}

b.StopTimer() dan b.StartTimer()

b.StopTimer() menghentikan timer benchmark, dan b.StartTimer() memulai kembali. Ini berguna untuk mengukur kinerja bagian-bagian tertentu dari kode Anda sambil mengecualikan bagian-bagian lain.


func BenchmarkWithStopStartTimer(b *testing.B) {
	for i := 0; i < b.N; i++ {
		b.StopTimer()
		// Operasi yang mahal yang tidak ingin diukur
		data := prepareData()
		b.StartTimer()

		// Kode yang akan di-benchmark
		processData(data)
	}
}

b.ReportAllocs()

b.ReportAllocs() melaporkan statistik alokasi memori untuk benchmark. Ini berguna untuk mengidentifikasi alokasi memori yang tidak perlu.


func BenchmarkWithReportAllocs(b *testing.B) {
	b.ReportAllocs()
	for i := 0; i < b.N; i++ {
		// Kode yang akan di-benchmark
		_ = make([]byte, 1024)
	}
}

Jalankan benchmark dengan flag -benchmem untuk melihat alokasi memori:

go test -bench=. -benchmem

Output yang mungkin:


goos: darwin
goarch: amd64
pkg: your_package
BenchmarkWithReportAllocs-8   	 1889457	       635 ns/op	   1024 B/op	      1 allocs/op
PASS
ok  	your_package	1.459s

Output menunjukkan bahwa setiap operasi mengalokasikan 1024 byte dan 1 alokasi.

b.RunParallel()

b.RunParallel() menjalankan benchmark secara paralel. Ini berguna untuk mengukur kinerja kode yang dirancang untuk dijalankan secara bersamaan.


func BenchmarkParallel(b *testing.B) {
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			// Kode yang akan di-benchmark
			_ = doSomeWork()
		}
	})
}

b.RunParallel membuat sejumlah goroutine dan mendistribusikan iterasi loop di antara mereka. Jumlah goroutine defaultnya adalah GOMAXPROCS.

Praktik Terbaik untuk Benchmarking

Untuk mendapatkan hasil benchmark yang akurat dan dapat diandalkan, ikuti praktik terbaik berikut:

  1. Jalankan Benchmark Berulang Kali: Jalankan benchmark beberapa kali untuk mengurangi noise dan variabilitas.
  2. Tutup Aplikasi dan Layanan yang Tidak Perlu: Pastikan tidak ada proses lain yang bersaing untuk sumber daya sistem.
  3. Gunakan Mesin Khusus: Jika mungkin, gunakan mesin khusus untuk benchmarking untuk meminimalkan gangguan.
  4. Hindari Optimasi Prematur: Benchmark kode Anda sebelum mengoptimalkan untuk mengidentifikasi bottleneck yang sebenarnya.
  5. Tulis Benchmark yang Jelas dan Ringkas: Buat benchmark Anda tetap fokus dan mudah dimengerti.
  6. Gunakan Data Input yang Realistis: Gunakan data input yang mencerminkan kasus penggunaan dunia nyata.
  7. Perhatikan Pemanasan (Warm-up): Pertimbangkan untuk melakukan "pemanasan" sebelum benchmarking untuk mengisi cache dan mengoptimalkan JIT.

Contoh Kasus: Benchmarking Fungsi Sortir

Mari kita buat contoh kasus yang lebih kompleks untuk benchmarking fungsi sortir yang berbeda.


package main

import (
	"math/rand"
	"sort"
	"testing"
)

const size = 10000

func generateRandomSlice(size int) []int {
	slice := make([]int, size)
	for i := 0; i < size; i++ {
		slice[i] = rand.Intn(size * 10)
	}
	return slice
}

func BenchmarkSort(b *testing.B) {
	data := generateRandomSlice(size)
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		// Buat salinan data untuk menghindari modifikasi data asli
		copyData := make([]int, len(data))
		copy(copyData, data)
		sort.Ints(copyData)
	}
}

func BenchmarkSortParallel(b *testing.B) {
    data := generateRandomSlice(size)
    b.ResetTimer()
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            // Buat salinan data untuk menghindari modifikasi data asli
            copyData := make([]int, len(data))
            copy(copyData, data)
            sort.Ints(copyData)
        }
    })
}

Dalam contoh ini, kita membandingkan kinerja fungsi sort.Ints dari paket sort dengan versi paralel.

Menjalankan benchmark:

go test -bench=.

Output yang mungkin:


goos: darwin
goarch: amd64
pkg: your_package
BenchmarkSort-8         	      23	  48913577 ns/op
BenchmarkSortParallel-8 	      51	  22983670 ns/op
PASS
ok  	your_package	2.384s

Dalam contoh ini, versi paralel jauh lebih cepat daripada versi serial pada mesin 8 inti.

Membandingkan Algoritma yang Berbeda

Benchmarking sangat berguna untuk membandingkan efisiensi algoritma yang berbeda untuk menyelesaikan masalah yang sama. Misalnya, mari kita bandingkan dua cara untuk mencari elemen dalam slice:

  1. Pencarian Linier
  2. Pencarian Biner (memerlukan slice yang diurutkan)

package main

import (
	"math/rand"
	"sort"
	"testing"
)

const searchSize = 1000
const searchValue = 500

func linearSearch(slice []int, value int) bool {
	for _, element := range slice {
		if element == value {
			return true
		}
	}
	return false
}

func binarySearch(slice []int, value int) bool {
	index := sort.SearchInts(slice, value)
	return index < len(slice) && slice[index] == value
}

func generateSortedSlice(size int) []int {
	slice := make([]int, size)
	for i := 0; i < size; i++ {
		slice[i] = i
	}
	return slice
}

func BenchmarkLinearSearch(b *testing.B) {
	data := generateSortedSlice(searchSize)
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		_ = linearSearch(data, searchValue)
	}
}

func BenchmarkBinarySearch(b *testing.B) {
	data := generateSortedSlice(searchSize)
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		_ = binarySearch(data, searchValue)
	}
}

Sekarang, mari kita jalankan benchmark:

go test -bench=.

Outputnya mungkin terlihat seperti ini:


goos: darwin
goarch: amd64
pkg: your_package
BenchmarkLinearSearch-8   	   28377	     42051 ns/op
BenchmarkBinarySearch-8   	11750045	        98.5 ns/op
PASS
ok  	your_package	3.089s

Hasilnya dengan jelas menunjukkan bahwa Pencarian Biner jauh lebih efisien daripada Pencarian Linier untuk slice yang diurutkan, terutama untuk dataset yang lebih besar. Ini karena Pencarian Biner memiliki kompleksitas waktu O(log n), sedangkan Pencarian Linier memiliki kompleksitas waktu O(n).

Benchmarking Operasi I/O

Benchmarking operasi I/O sangat penting untuk mengoptimalkan aplikasi yang sangat bergantung pada interaksi disk atau jaringan. Berikut adalah contoh cara benchmark operasi pembacaan file:


package main

import (
	"io"
	"os"
	"testing"
)

const filename = "testfile.txt"
const filesize = 1024 * 1024 // 1MB

func createFile(filename string, filesize int) error {
	file, err := os.Create(filename)
	if err != nil {
		return err
	}
	defer file.Close()

	data := make([]byte, 1024)
	for i := 0; i < 1024; i++ {
		data[i] = 'A'
	}

	for i := 0; i < filesize/1024; i++ {
		_, err := file.Write(data)
		if err != nil {
			return err
		}
	}
	return nil
}

func BenchmarkFileRead(b *testing.B) {
	err := createFile(filename, filesize)
	if err != nil {
		b.Fatalf("Failed to create file: %v", err)
	}
	defer os.Remove(filename)

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		file, err := os.Open(filename)
		if err != nil {
			b.Fatalf("Failed to open file: %v", err)
		}
		_, err = io.Copy(io.Discard, file) // Membaca seluruh file dan membuang isinya
		if err != nil {
			b.Fatalf("Failed to read file: %v", err)
		}
		file.Close()
	}
}

Dalam contoh ini, kita membuat file sementara, kemudian benchmark waktu yang dibutuhkan untuk membaca seluruh file menggunakan io.Copy dan membuang isinya. Ini mensimulasikan membaca data tanpa memprosesnya.

Menjalankan benchmark:

go test -bench=.

Output yang mungkin:


goos: darwin
goarch: amd64
pkg: your_package
BenchmarkFileRead-8   	      32	  36172186 ns/op
PASS
ok  	your_package	1.229s

Benchmark operasi I/O membantu Anda memahami batasan kinerja yang terkait dengan interaksi disk atau jaringan.

Benchmarking dengan Profiling

Sementara benchmarking memberikan metrik kinerja kuantitatif, profiling memberikan wawasan yang lebih dalam tentang di mana kode Anda menghabiskan waktunya. Go menyediakan alat profiling bawaan yang dapat digunakan bersama dengan benchmarking.

Jenis Profiling

  1. CPU Profiling: Mengidentifikasi fungsi-fungsi yang menghabiskan sebagian besar waktu CPU.
  2. Memory Profiling (Heap Profiling): Menganalisis alokasi memori.
  3. Block Profiling: Mengidentifikasi operasi pemblokiran (misalnya, kunci mutex, I/O).
  4. Mutex Profiling: Mengidentifikasi persaingan mutex.

Menggunakan Pprof

Paket pprof menyediakan fasilitas untuk profiling. Untuk menggunakan pprof, Anda harus mengimpor paket net/http/pprof dan memulai server HTTP.


package main

import (
	"log"
	"net/http"
	_ "net/http/pprof" // Import untuk mendaftarkan handler pprof
)

func main() {
	go func() {
		log.Println(http.ListenAndServe("localhost:6060", nil))
	}()

	// Kode aplikasi Anda di sini
	// ...
}

Setelah aplikasi berjalan, Anda dapat mengakses data profiling melalui browser atau menggunakan alat baris perintah go tool pprof.

Untuk CPU profiling, gunakan:

go tool pprof http://localhost:6060/debug/pprof/profile

Untuk memory profiling, gunakan:

go tool pprof http://localhost:6060/debug/pprof/heap

Alat go tool pprof menyediakan berbagai perintah untuk menganalisis data profiling, seperti top, web, dan list.

Menggunakan Profiling dengan Benchmark

Anda juga dapat menggunakan profiling langsung dari kode benchmark Anda menggunakan fungsi testing.B. Berikut adalah contoh yang menunjukkan CPU profiling:


package main

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

func doSomeWork() {
	for i := 0; i < 1000000; i++ {
		_ = i * 2
	}
}

func BenchmarkWithCPUProfile(b *testing.B) {
	f, err := os.Create("cpu.prof")
	if err != nil {
		b.Fatalf("could not create CPU profile: %v", err)
	}
	defer f.Close()
	if err := pprof.StartCPUProfile(f); err != nil {
		b.Fatalf("could not start CPU profile: %v", err)
	}
	defer pprof.StopCPUProfile()

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		doSomeWork()
	}
}

Setelah menjalankan benchmark ini, file bernama cpu.prof akan dibuat. Anda dapat menganalisis file ini menggunakan go tool pprof:

go tool pprof cpu.prof

Profiling, yang dikombinasikan dengan benchmarking, memberdayakan Anda untuk mengidentifikasi bottleneck kinerja secara presisi dan mengoptimalkan kode Anda secara efektif.

Ringkasan

Benchmarking adalah alat penting untuk menulis kode Go berperforma tinggi. Dengan memahami dasar-dasar benchmarking, menggunakan fitur-fitur lanjutan dari paket testing, dan mengikuti praktik terbaik, Anda dapat mengoptimalkan kode Anda dan memastikan bahwa itu memenuhi kebutuhan kinerja Anda. Gabungkan benchmarking dengan profiling untuk wawasan yang lebih dalam dan optimasi yang lebih terfokus.

Semoga panduan ini telah memberi Anda dasar yang kuat untuk benchmarking di Go. Selamat mencoba dan mengoptimalkan!

```

omcoding

Leave a Reply

Your email address will not be published. Required fields are marked *