Benchmarks
Benchmark คืออะไร?
หัวข้อที่มีชื่อว่า “Benchmark คืออะไร?”benchmark วัดว่าโค้ดใช้เวลานานเท่าไร Go benchmarks อยู่ในไฟล์ _test.go เคียงข้าง unit tests และรันด้วยคำสั่ง go test เดียวกัน — เพียงแต่ใช้ flags ต่างกัน
ฟังก์ชัน benchmark มี signature ดังนี้:
func BenchmarkXxx(b *testing.B) { // ... setup code (ไม่ถูกวัด) ... for b.Loop() { // Go 1.24+ รูปแบบที่แนะนำ // โค้ดที่ต้องการวัด }}ค่า testing.B ขับเคลื่อน loop framework จะเริ่มด้วยค่า b.N ขนาดเล็กและเพิ่มขึ้นเรื่อยๆ จนกว่า benchmark จะรันนานพอที่จะให้การวัดที่เสถียร คุณไม่ต้องกำหนด b.N เอง
รูปแบบ loop สองแบบ
หัวข้อที่มีชื่อว่า “รูปแบบ loop สองแบบ”Go 1.24 แนะนำ b.Loop() เป็นทางเลือกที่ดีกว่า classic b.N counter loop:
// รูปแบบ classic — ใช้ได้กับ Go ทุกเวอร์ชันfunc BenchmarkFibClassic(b *testing.B) { for i := 0; i < b.N; i++ { Fibonacci(20) }}
// รูปแบบ modern — Go 1.24+func BenchmarkFib(b *testing.B) { for b.Loop() { Fibonacci(20) }}ทั้งสองรูปแบบถูกต้อง b.Loop() แนะนำใน Go 1.24+ เพราะจัดการ timer reset ภายในเองและมีโอกาสผิดพลาดน้อยกว่า
Setup ภายนอก loop
หัวข้อที่มีชื่อว่า “Setup ภายนอก loop”หาก benchmark ต้องการ setup ที่ซับซ้อน (เปิดไฟล์, สร้าง slice ขนาดใหญ่) ให้ทำก่อน loop และเรียก b.ResetTimer() เพื่อไม่ให้เวลา setup ถูกนับ:
func BenchmarkIsPrimeList(b *testing.B) { numbers := make([]int, 1000) for i := range numbers { numbers[i] = i + 2 } b.ResetTimer() // เริ่มวัดจากจุดนี้
for b.Loop() { for _, n := range numbers { IsPrime(n) } }}การรัน benchmarks
หัวข้อที่มีชื่อว่า “การรัน benchmarks”Unit tests ไม่ รัน benchmarks โดยค่าเริ่มต้น ใช้ flag -bench:
# รัน benchmarks ทั้งหมด แสดง memory allocationsgo test -bench=. -benchmem ./...
# รันเฉพาะ benchmarks ที่ชื่อตรงกับ patterngo test -bench=BenchmarkFib -benchmem ./...
# รัน benchmarks อย่างน้อย 5 วินาทีต่อฟังก์ชันgo test -bench=. -benchtime=5s ./...การอ่านผลลัพธ์
หัวข้อที่มีชื่อว่า “การอ่านผลลัพธ์”BenchmarkFib-8 5000000 234 ns/opBenchmarkFibClassic-8 5000000 236 ns/op 0 B/op 0 allocs/op| คอลัมน์ | ความหมาย |
|---|---|
suffix -8 | ค่า GOMAXPROCS (8 logical CPUs) |
5000000 | จำนวน iterations ที่ b.N รัน |
234 ns/op | nanoseconds ต่อ operation |
0 B/op | bytes ที่ allocate ต่อ operation (ต้องใช้ -benchmem) |
0 allocs/op | heap allocations ต่อ operation (ต้องใช้ -benchmem) |
Fibonacci ไม่ allocate อะไรเลยเพราะใช้แค่ stack variables — นั่นคือเหตุผลที่ทั้งสองคอลัมน์ alloc เป็นศูนย์ ฟังก์ชันที่สร้าง []string จะแสดงค่าที่ไม่ใช่ศูนย์
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| สิ่งที่ได้ | ประโยชน์ | ต้นทุน |
|---|---|---|
go test -bench | วัด performance ด้วยเครื่องมือ built-in | ต้องรันซ้ำหลายครั้ง ระวัง noise จาก GC และ CPU throttling |
b.N auto-calibration | framework กำหนด iteration count ให้ได้ผลที่เสถียร | ถ้าใส่ setup ใน loop = benchmark นับ setup ด้วย |
b.ReportAllocs() | เห็น heap allocation ต่อ operation ชัดเจน | ต้องเข้าใจ Escape Analysis เพื่อตีความผลลัพธ์ |
| pprof profiling | วิเคราะห์ CPU/memory hotspot เชิงลึก | overhead ขณะ profiling — อย่าใช้ใน production โดยไม่ตั้งใจ |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- benchmark result บน dev machine = production — ต้องคำนึง CPU throttling, GC state, background processes ที่ต่างกัน
b.Nคือจำนวนที่กำหนดเอง — Go calibrateb.Nอัตโนมัติให้ benchmark ใช้เวลาประมาณ 1 วินาที — อย่ากำหนดค่าb.Nเองallocs/op = 0หมายถึง optimal เสมอ — zero allocation ดี แต่ถ้า CPU work เพิ่มขึ้นแทน อาจไม่ได้ดีกว่า- benchmark สำหรับ micro-optimization เท่านั้น — benchmark ช่วย validate architectural decision ได้ด้วย เช่น channel vs mutex, slice vs map
💡 ตัวอย่างจากของจริง
Kubernetes ใช้ benchmark สำหรับ JSON marshaling ของ API object และ etcd client latency — ป้องกัน performance regression ระหว่าง release
Go standard library เพิ่ม benchmark ทุก PR ที่แตะ hot path เช่น
encoding/json,strings,bytesPrometheus benchmark metric registration และ observation ใน hot path — ต้องรองรับ millions of observations per second