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:
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.
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:
Oddiy dasturni ham tekshirish mumkin:
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.