Benchmark тест
Кодоо бичсэн. Тест давсан. Гэвч "энэ нь хангалттай хурдан уу?" гэсэн асуулт гарч ирнэ. Жишээ нь, string нэгтгэхдээ + ашиглах уу, strings.Builder ашиглах уу? Хоёулаа зөв ажиллана, гэхдээ хурд нь маш ялгаатай байж болно. Benchmark тест нь энэ асуултанд нарийн хариулт өгдөг. Go-д benchmark нь testing package-д суурилагдсан байдаг — нэмэлт хэрэгсэл хэрэггүй.
Анхны benchmark
Benchmark функц Benchmark угтвартай байж, *testing.B параметр авдаг. b.N нь Go-ийн framework-оос автоматаар тогтоогддог давталтын тоо:
// string_test.go
package stringutil
import (
"strings"
"testing"
)
// Ердийн + ашиглан string нэгтгэх
func НэгтгэхНэмэх(үгс []string) string {
үр_дүн := ""
for _, үг := range үгс {
үр_дүн += үг + " "
}
return үр_дүн
}
// strings.Builder ашиглан нэгтгэх
func НэгтгэхBuilder(үгс []string) string {
var sb strings.Builder
for _, үг := range үгс {
sb.WriteString(үг)
sb.WriteString(" ")
}
return sb.String()
}
// Benchmark: + operator
func BenchmarkНэгтгэхНэмэх(b *testing.B) {
үгс := []string{"Сайн", "уу", "дэлхий", "Go", "хэл"}
for i := 0; i < b.N; i++ { // b.N-г өөрчлөхгүй
НэгтгэхНэмэх(үгс)
}
}
// Benchmark: strings.Builder
func BenchmarkНэгтгэхBuilder(b *testing.B) {
үгс := []string{"Сайн", "уу", "дэлхий", "Go", "хэл"}
for i := 0; i < b.N; i++ {
НэгтгэхBuilder(үгс)
}
}
Benchmark ажиллуулах:
go test -bench=. -benchmem
Үр дүн иймэрхүү харагдана:
BenchmarkНэгтгэхНэмэх-8 3000000 450 ns/op 128 B/op 5 allocs/op
BenchmarkНэгтгэхBuilder-8 8000000 180 ns/op 64 B/op 2 allocs/op
strings.Builder хоёр дахин хурдан, санах ой ч бага зарцуулж байна! Тоо нотолж байна.
Үр дүнг уншиж ойлгох
Benchmark гаралт хэд хэдэн баганатай:
BenchmarkНэгтгэхBuilder-8 8000000 180 ns/op 64 B/op 2 allocs/op
│ │ │ │ │ │
│ │ │ │ │ └─ Нэг ажилд heap allocation
│ │ │ │ └─ Нэг ажилд зарцуулсан санах ой
│ │ │ └─ Нэг ажил хэдэн nanosecond зарцуулсан
│ │ └─ Нийт хэдэн удаа ажиллуулсан
│ └─ CPU-ийн thread тоо
└─ Benchmark функцийн нэр
- ns/op бага байх тусам хурдан
- B/op бага байх тусам санах ой хэмнэлттэй
- allocs/op бага байх тусам garbage collector-д дарамт бага
b.ResetTimer — бэлтгэлийн цагийг хасах
Benchmark эхлэхийн өмнө нэмэлт бэлтгэл шаардлагатай бол b.ResetTimer() ашигладаг. Энэ нь бэлтгэлийн цагийг хэмжилтээс хасна:
func BenchmarkТомБоловсруулалт(b *testing.B) {
// Бэлтгэл: том slice үүсгэх
өгөгдөл := make([]int, 100_000)
for i := range өгөгдөл {
өгөгдөл[i] = i
}
// Бэлтгэлийн цагийг дахин тохируулах
b.ResetTimer()
// Зөвхөн энэ хэсгийн цагийг хэмжинэ
for i := 0; i < b.N; i++ {
нийлбэр := 0
for _, v := range өгөгдөл {
нийлбэр += v
}
_ = нийлбэр // compiler optimize хийхгүйн тулд
}
}
_ = нийлбэр нь Go-ийн compiler-ийг дахин хялбаршуулахаас сэргийлдэг. Benchmark нь зөвхөн алгоритмын гүйцэтгэлийг хэмжих ёстой.
Дэд benchmark — олон хувилбар харьцуулах
b.Run() ашиглан нэг функцэд олон хувилбарыг хамтад нь харьцуулж болно:
func BenchmarkЭрэмбэлэх(b *testing.B) {
хэмжээнүүд := []int{10, 100, 1000, 10_000}
for _, хэмжээ := range хэмжээнүүд {
// Хэмжээ бүрт дэд benchmark үүсгэх
b.Run(fmt.Sprintf("хэмжээ=%d", хэмжээ), func(b *testing.B) {
өгөгдөл := make([]int, хэмжээ)
for i := range өгөгдөл {
өгөгдөл[i] = хэмжээ - i // буруу эрэмбэлэгдсэн
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
хуулбар := make([]int, len(өгөгдөл))
copy(хуулбар, өгөгдөл)
sort.Ints(хуулбар)
}
})
}
}
BenchmarkЭрэмбэлэх/хэмжээ=10-8 5000000 250 ns/op
BenchmarkЭрэмбэлэх/хэмжээ=100-8 500000 3200 ns/op
BenchmarkЭрэмбэлэх/хэмжээ=1000-8 40000 38000 ns/op
BenchmarkЭрэмбэлэх/хэмжээ=10000-8 3000 450000 ns/op
Өгөгдлийн хэмжээ 10 дахин нэмэгдэхэд ажлын цаг хэдэн дахин нэмэгдэж байгааг хялбар харж болно. Энэ нь алгоритмын complexity-г тодорхойлоход тусладаг.
Benchmark болон тестийг хамтад нь ажиллуулах
# Зөвхөн тест
go test ./...
# Зөвхөн benchmark (тест ажиллуулахгүй)
go test -bench=. -run=^$ ./...
# Benchmark болон тест хоёуланг
go test -bench=. ./...
# Тодорхой benchmark
go test -bench=BenchmarkНэгтгэх -benchtime=5s ./...
-benchtime=5s нь benchmark-ийг 5 секунд ажиллуулж илүү найдвартай үр дүн гаргана. Анхдагч нь 1 секунд.
Дараагийн хичээлд:
Тест болон benchmark-ийг судаллаа. Дараагийн хичээлд Go-ийн generic үндсийг судална. Generic нь нэг функцыг олон төрлийн өгөгдөлд ашиглах боломж олгодог — код давтахгүйгээр ерөнхий шийдэл бичих хүчирхэг арга юм.