Go / Benchmark тест

Benchmark тест

Кодоо бичсэн. Тест давсан. Гэвч "энэ нь хангалттай хурдан уу?" гэсэн асуулт гарч ирнэ. Жишээ нь, string нэгтгэхдээ + ашиглах уу, strings.Builder ашиглах уу? Хоёулаа зөв ажиллана, гэхдээ хурд нь маш ялгаатай байж болно. Benchmark тест нь энэ асуултанд нарийн хариулт өгдөг. Go-д benchmark нь testing package-д суурилагдсан байдаг — нэмэлт хэрэгсэл хэрэггүй.

Анхны benchmark

Benchmark функц Benchmark угтвартай байж, *testing.B параметр авдаг. b.N нь Go-ийн framework-оос автоматаар тогтоогддог давталтын тоо:

go
// 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 ажиллуулах:

bash
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() ашигладаг. Энэ нь бэлтгэлийн цагийг хэмжилтээс хасна:

go
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() ашиглан нэг функцэд олон хувилбарыг хамтад нь харьцуулж болно:

go
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(хуулбар)
            }
        })
    }
}
bash
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 болон тестийг хамтад нь ажиллуулах

bash
# Зөвхөн тест
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 нь нэг функцыг олон төрлийн өгөгдөлд ашиглах боломж олгодог — код давтахгүйгээр ерөнхий шийдэл бичих хүчирхэг арга юм.