Tarkibga o'tish

Benchmark va race detector

Benchmark kodning ma’lum qismini takroran bajarib, uning vaqt va xotira sarfini o‘lchaydi. Race detector esa bir nechta goroutine umumiy xotiraga xavfli tarzda murojaat qilganini bajarilish vaqtida aniqlashga yordam beradi.

Benchmark yozish

Benchmark _test.go faylida BenchmarkXxx(b *testing.B) ko‘rinishida yoziladi:

join_test.go
package text

import (
    "strings"
    "testing"
)

var result string

func BenchmarkJoin(b *testing.B) {
    parts := []string{"go", "lang", "uz"}
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
        result = strings.Join(parts, "-")
    }
}

b.N qiymatini benchmark vositasi o‘lchov yetarlicha barqaror bo‘lguncha oshiradi. Natijani package-level o‘zgaruvchiga yozish kompilyator hisoblashni keraksiz deb olib tashlashining oldini olishga yordam beradi.

go test -bench=.
go test -bench=. -benchmem

Natijada quyidagi ustunlar uchrashi mumkin:

  • ns/op — bitta amal uchun o‘rtacha nanosekund;
  • B/op — bitta amalga ajratilgan baytlar;
  • allocs/op — bitta amalga to‘g‘ri kelgan xotira ajratishlar.

b.ReportAllocs() yoki -benchmem xotira ko‘rsatkichlarini chiqaradi. Bir martalik setup o‘lchovga kirmasligi kerak bo‘lsa, uni sikldan oldin bajaring va kerakli joyda b.ResetTimer() ishlating.

Natijani to‘g‘ri talqin qilish

Benchmark faqat tekshirilgan kod, ma’lumot, kompyuter va muhitdagi natijani ko‘rsatadi. U real server tezligini yoki foydalanuvchi tajribasini o‘z-o‘zidan isbotlamaydi.

Mikrobenchmarkda quyidagi xatolar ko‘p uchraydi:

  • kompilyator natijasi ishlatilmagan hisoblashni olib tashlaydi;
  • setup va I/O o‘lchanayotgan kichik amaldan ko‘proq vaqt oladi;
  • juda sodda test real ma’lumot taqsimotini ifodalamaydi;
  • fon jarayonlari va CPU rejimi natijani o‘zgartiradi.

Taqqoslash uchun benchmarkni bir necha marta bajaring va bir xil muhitdan foydalaning. Avval o‘lchang, keyin amaliy ahamiyatga ega tor joyni optimallashtiring.

Data raceni aniqlash

Data race kamida ikki goroutine bir xil xotira manziliga sinxronizatsiyasiz murojaat qilganda va ulardan kamida bittasi yozganda yuz beradi. Testlarni race detector bilan ishga tushirish:

go test -race ./...

Oddiy dasturni ham tekshirish mumkin:

go run -race main.go

Race detector dasturni instrumentatsiya qilgani uchun bajarilish sekinlashadi va ko‘proq xotira sarflaydi. Uni production performance o‘lchovi sifatida ishlatmang.

Warning

Race detector faqat tekshiruv vaqtida bajarilgan kod yo‘llarini kuzatadi. Ogohlantirish chiqmagani barcha mumkin bo‘lgan bajarilishlarda data race yo‘qligini isbotlamaydi. Muhim concurrent yo‘llarni testlar bilan bajaring.

Race hisobotida qarama-qarshi murojaatlar va ularni yaratgan goroutinelar stack trace’i ko‘rsatiladi. Faqat ogohlantirishni yashirmang; umumiy holat egaligini aniqlang va sync.Mutex, channel yoki boshqa mos sinxronizatsiya bilan mantiqni tuzating.

Xulosa

Benchmark vaqt va xotira sarfini o‘lchaydi, lekin real tizim tezligini yakka o‘zi isbotlamaydi. go test -bench=. -benchmem natijasini bir xil muhitda taqqoslang. go test -race ./... esa bajarilgan concurrent yo‘llardagi data racelarni topishga yordam beradi.

Misollar

1. Butun sonlar yig‘indisini o‘lchash

Bu misolda testing.Benchmark() oddiy yig‘indi amalini ko‘p marta bajaradi.

package main

import (
    "fmt"
    "testing"
)

var sumResult int

func main() {
    result := testing.Benchmark(func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            sum := 0
            for number := 1; number <= 100; number++ {
                sum += number
            }
            sumResult = sum
        }
    })

    fmt.Println(result)
}

sum 0dan boshlanadi, chunki 0 qo‘shish natijani o‘zgartirmaydi. Ichki siklda <= 100 yozilgani uchun 100 ham yig‘indiga kiradi. Natija global sumResultga yoziladi. Bu kompilyator hisoblashni keraksiz deb olib tashlashining oldini oladi.

2. Stringlarni + bilan birlashtirish sarfi

Bu misolda bir nechta stringni + operatori bilan yig‘ish o‘lchanadi.

package main

import (
    "fmt"
    "testing"
)

var textResult string

func main() {
    result := testing.Benchmark(func(b *testing.B) {
        b.ReportAllocs()
        for i := 0; i < b.N; i++ {
            textResult = "Go" + " " + "dasturlash" + " " + "tili"
        }
    })

    fmt.Printf("%s, ajratishlar: %d\n", result, result.AllocsPerOp())
}

b.ReportAllocs() xotira ajratishlarini natijaga qo‘shadi. AllocsPerOp() bitta takrorlashga to‘g‘ri kelgan ajratishlar sonini qaytaradi. Qismlar o‘zgarmas bo‘lgani uchun kompilyator ayrim birlashtirishlarni oldindan bajarishi mumkin; benchmark natijasi aynan shu kodga tegishli.

3. strings.Builder uchun sig‘im ajratish

Bu misolda Grow() bilan oldindan sig‘im ajratilgan strings.Builder o‘lchanadi.

package main

import (
    "fmt"
    "strings"
    "testing"
)

var builderResult string

func main() {
    result := testing.Benchmark(func(b *testing.B) {
        b.ReportAllocs()
        for i := 0; i < b.N; i++ {
            var builder strings.Builder
            builder.Grow(18)
            builder.WriteString("Go")
            builder.WriteString(" dasturlash tili")
            builderResult = builder.String()
        }
    })

    fmt.Println(result)
}

18 ikki qismdagi baytlarning umumiy soniga teng. Yetarli sig‘im oldindan ajratilsa, builder o‘sish paytida qo‘shimcha xotira ajratmasligi mumkin. String() yakuniy matnni qaytaradi.

4. Setup vaqtini ResetTimer() bilan chiqarib tashlash

Bu misolda katta slice tayyorlash vaqti o‘lchovga kiritilmaydi.

package main

import (
    "fmt"
    "testing"
)

var lastValue int

func main() {
    result := testing.Benchmark(func(b *testing.B) {
        numbers := make([]int, 1000)
        for i := range numbers {
            numbers[i] = i
        }

        b.ResetTimer()
        for i := 0; i < b.N; i++ {
            lastValue = numbers[len(numbers)-1]
        }
    })

    fmt.Println(result)
}

Slice uzunligi 1000, shuning uchun haqiqiy indekslar 0dan 999gacha. Oxirgi element len(numbers)-1 orqali olinadi. ResetTimer() undan oldingi tayyorlash vaqtini va hisoblangan bayt hamda allocation ko‘rsatkichlarini tozalaydi.

5. Tayyorlashni vaqtincha to‘xtatish

Bu misolda har bir takrorlashdagi tayyorlash amali StopTimer() va StartTimer() orasida bajariladi.

package main

import (
    "fmt"
    "testing"
)

var lookupResult bool

func main() {
    result := testing.Benchmark(func(b *testing.B) {
        b.StopTimer()
        values := map[string]int{"go": 1, "api": 2}
        b.StartTimer()

        for i := 0; i < b.N; i++ {
            _, lookupResult = values["go"]
        }
    })

    fmt.Println(result)
}

Map yaratish bu misolda o‘lchanadigan amal emas, shuning uchun taymer vaqtincha to‘xtatiladi. Taymer qayta yoqilgach sikldagi go kalitini qidirish o‘lchanadi. lookupResult kalit topilganini saqlaydi.

6. Kirish hajmini SetBytes() bilan ko‘rsatish

Bu misolda benchmark har bir amalda nechta bayt ko‘rilishini natijaga qo‘shadi.

package main

import (
    "fmt"
    "strings"
    "testing"
)

var containsResult bool

func main() {
    text := strings.Repeat("a", 1024)
    result := testing.Benchmark(func(b *testing.B) {
        b.SetBytes(int64(len(text)))
        for i := 0; i < b.N; i++ {
            containsResult = strings.Contains(text, "z")
        }
    })

    fmt.Println(result)
}

1024 bayt bir kibibaytga teng. Matnda z yo‘q, shu sabab qidiruv butun stringni ko‘rib chiqadi. SetBytes() natijada o‘tkazuvchanlikni MB/s ko‘rinishida hisoblash imkonini beradi.

7. Maxsus ko‘rsatkich chiqarish

Bu misolda ReportMetric() orqali bitta amalga tegishli elementlar soni chiqariladi.

package main

import (
    "fmt"
    "testing"
)

var productResult int

func main() {
    result := testing.Benchmark(func(b *testing.B) {
        numbers := []int{2, 3, 4, 5}
        b.ReportMetric(float64(len(numbers)), "elements/op")

        for i := 0; i < b.N; i++ {
            product := 1
            for _, number := range numbers {
                product *= number
            }
            productResult = product
        }
    })

    fmt.Println(result)
}

Ko‘paytma 1dan boshlanadi, chunki 1ga ko‘paytirish qiymatni o‘zgartirmaydi. Slice to‘rtta elementdan iborat. Shu sabab maxsus ko‘rsatkich har bir amalda 4 elements/op deb chiqadi.

8. Benchmarkni bir necha marta takrorlash

Bu misoldagi dastur bir xil benchmarkni bajaradi, buyruq esa tashqi benchmark runner orqali natijani bir necha marta yig‘ishni ko‘rsatadi.

package main

import (
    "fmt"
    "testing"
)

var compareResult int

func main() {
    result := testing.Benchmark(func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            compareResult = (25 * 4) + 10
        }
    })

    fmt.Println(result)
}

Haqiqiy _test.go benchmarkini go test -bench=. -count=5 bilan besh marta bajarish mumkin. 5 bitta tasodifiy tez yoki sekin natijaga tayanmaslik uchun tanlangan. Barcha takrorlashlar bir xil muhitda bajarilsa, tarqalishni ko‘rish osonlashadi.

9. Umumiy o‘zgaruvchidagi data raceni ko‘rish

Bu misolda ikki goroutine bir xil o‘zgaruvchiga yozadi. Kod ataylab xavfli holatni ko‘rsatadi.

package main

import (
    "fmt"
    "time"
)

func main() {
    value := 0

    go func() {
        value = 1
    }()

    go func() {
        value = 2
    }()

    time.Sleep(100 * time.Millisecond)
    fmt.Println(value)
}

Dasturni go run -race main.go bilan ishga tushirganda race detector qarama-qarshi yozuvlarni ko‘rsatadi. valuening 0 qiymati int turining nol qiymati. 100 millisekund faqat goroutinelarga bajarilish imkonini beradi; u yozuvlarni sinxronlashtirmaydi va data raceni tuzatmaydi.

10. Turli slice elementlariga yozish

Bu misolda goroutinelar bitta slicening turli elementlariga yozadi.

package main

import (
    "fmt"
    "time"
)

func main() {
    values := make([]int, 2)

    go func() {
        values[0] = 10
    }()

    go func() {
        values[1] = 20
    }()

    time.Sleep(100 * time.Millisecond)
    fmt.Println("Yozishlar yakunlandi")
}

Slice uzunligi 2, shuning uchun unda 0 va 1 indekslari bor. Har bir goroutine boshqa xotira manziliga yozadi. main() slice elementlarini qayta o‘qimaydi. go run -race main.go bu ikki yozuv uchun data race chiqarmaydi. Sleep() yakunni kutishning umumiy yechimi emas; bu yerda faqat race detectorning kichik misolini tugatish uchun ishlatilgan.