Benchmark
Benchmark dasturdagi ma’lum bir amal qancha vaqt olishini va qancha xotira ajratishini o‘lchashga yordam beradi. Buning uchun o‘lchanayotgan kod ko‘p marta takroran bajariladi.
Benchmark yozish
Go benchmarklari odatda _test.go faylida yoziladi. Benchmark funksiyasining nomi Benchmark bilan boshlanadi va u *testing.B qabul qiladi:
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, "-")
}
}
Bu benchmark strings.Join() amalining ishlashini o‘lchaydi.
Muhim qator:
Bu yerda b.N oddiy oldindan belgilangan son emas. Benchmark runner uning qiymatini o‘zi boshqaradi.
Dastlab b.N kichik bo‘lishi mumkin. Keyin Go benchmarkni qayta-qayta ishga tushiradi va b.Nni kattalashtiradi. Maqsad — o‘lchov yetarlicha uzoq davom etishi va natija nisbatan barqaror bo‘lishi.
Shuning uchun benchmark ichida odatda quyidagi shakl ishlatiladi:
Benchmark natijasi package darajasidagi result o‘zgaruvchisiga yozilgan:
va:
Buning sababi benchmark natijasidan foydalanilayotganini kompilyatorga ko‘rsatishdir. Agar hisoblangan qiymat hech qayerda ishlatilmasa, kompilyator ayrim holatlarda keraksiz hisoblashni optimizatsiya qilib tashlashi mumkin.
Benchmarkni quyidagicha ishga tushirish mumkin:
Bu buyruq joriy package ichidagi benchmarklarni ishga tushiradi.
Xotira ajratishlari haqidagi ma’lumotni ham ko‘rish uchun:
ishlatiladi.
Benchmark natijasida quyidagi ko‘rsatkichlar uchrashi mumkin:
ns/op— bitta amal o‘rtacha necha nanosekund davom etgani;B/op— bitta amalga o‘rtacha necha bayt xotira ajratilgani;allocs/op— bitta amal davomida o‘rtacha nechta allocation bo‘lgani.
Masalan, natija quyidagiga o‘xshashi mumkin:
Bu yerda aniq sonlar kompyuter, Go versiyasi va muhitga qarab o‘zgaradi.
b.ReportAllocs() benchmark natijasiga allocation ma’lumotlarini qo‘shishni so‘raydi:
Xuddi shu ma’lumotni command line orqali -benchmem bilan ham chiqarish mumkin.
Benchmarkda bir martalik tayyorlash kodi ham bo‘lishi mumkin. Masalan, katta slice yaratish yoki test ma’lumotlarini oldindan tayyorlash kerak bo‘lsa, bu vaqt o‘lchanayotgan asosiy amal tarkibiga kirmasligi mumkin.
Bunday holatda tayyorlashni sikldan oldin bajarib, keyin:
chaqirish mumkin.
ResetTimer() undan oldin yig‘ilgan vaqt va benchmark statistikalarini tozalaydi. Shundan keyin asosiy o‘lchov boshlanadi.
Natijani to‘g‘ri talqin qilish
Benchmark natijasini ko‘rish oson. Uni to‘g‘ri talqin qilish esa alohida e’tibor talab qiladi.
Benchmark faqat aynan tekshirilgan:
- kod;
- kirish ma’lumoti;
- kompyuter;
- Go versiyasi;
- operatsion tizim;
- CPU holati;
- boshqa jarayonlar
uchun natija beradi.
Shuning uchun mikrobenchmark natijasidan:
Bu yechim real serverda ham aynan shuncha tez ishlaydi.
degan xulosa chiqarish to‘g‘ri emas.
Mikrobenchmark odatda juda kichik bir amalni izolyatsiya qilib o‘lchaydi. Real dasturda esa boshqa xarajatlar ham bo‘ladi:
- network I/O;
- database;
- disk;
- locklar;
- scheduler;
- garbage collector;
- boshqa goroutinelar;
- request parsing;
- serialization.
Shu sabab mikrobenchmark ikki implementatsiyani solishtirishda foydali bo‘lishi mumkin, lekin foydalanuvchi ko‘radigan umumiy latency’ni yakka o‘zi isbotlamaydi.
Mikrobenchmarklarda quyidagi xatolar ko‘p uchraydi.
Kompilyator hisoblashni olib tashlashi mumkin
Agar natija hech qayerda ishlatilmasa, kompilyator ayrim hisoblashlarni keraksiz deb topishi mumkin.
Masalan, faqat qiymat hisoblanib, keyin undan foydalanilmasa, benchmark real ishni o‘lchamasligi ehtimoli bor.
Shu sabab muhim natijani package-level yoki boshqa tashqaridan kuzatiladigan joyga yozish ko‘p benchmarklarda qo‘llanadi.
Setup asosiy amaldan qimmatroq bo‘lishi mumkin
Tasavvur qiling, siz bitta map lookup’ni o‘lchamoqchisiz. Lekin har takrorlashda ulkan map yaratib olsangiz, natijaning katta qismi map yaratish vaqtini o‘lchaydi.
Bunday benchmark siz o‘ylagan amalni emas, asosan setup xarajatini ko‘rsatadi.
Shuning uchun setup’ni alohida bajarish yoki timer’ni vaqtincha to‘xtatish kerak bo‘lishi mumkin.
Juda sodda ma’lumot real holatni ifodalamasligi mumkin
Masalan, 3 ta elementli slice ustida ishlaydigan benchmark production’dagi million elementli slice xatti-harakatini to‘liq ifodalamasligi mumkin.
Xuddi shuningdek, doim bir xil string yoki doim cache’da turgan ma’lumot real workload’dan farq qilishi mumkin.
Kompyuter holati natijaga ta’sir qiladi
Fon jarayonlari, CPU frequency scaling, thermal throttling va scheduler holati benchmark natijasini o‘zgartirishi mumkin.
Shu sabab bitta o‘lchovga tayanish yaxshi fikr emas.
Benchmarklarni taqqoslashda ularni bir necha marta ishga tushirish foydali:
Muhim jihat — oldingi va keyingi natijalarni imkon qadar bir xil muhitda olish.
Avval muammoni o‘lchash kerak. Keyin haqiqatan ham performance uchun ahamiyatli bo‘lgan tor joyni optimallashtirish kerak.
Faqat mikrobenchmarkda bir necha nanosekund yutish har doim real dastur uchun sezilarli foyda bermaydi.
Misollar
1. Butun sonlar yig‘indisini o‘lchash
Bu misol odatiy _test.go faylida benchmark yozishni ko‘rsatadi.
Benchmark 1dan 100gacha bo‘lgan sonlarni yig‘ish amalini takroran bajaradi.
package benchmark
import "testing"
var sumResult int
func BenchmarkSum(b *testing.B) {
for i := 0; i < b.N; i++ {
sum := 0
for number := 1; number <= 100; number++ {
sum += number
}
sumResult = sum
}
}
Avval:
yoziladi.
Yig‘indi 0dan boshlanishining sababi oddiy:
0 qo‘shish natijani o‘zgartirmaydi.
Keyingi sikl:
number qiymatini bosqichma-bosqich oshiradi:
Bu yerda <= 100 ishlatilgani uchun 100 ham yig‘indiga kiradi.
Hisoblash yakunlangach:
bajariladi.
sumResult global o‘zgaruvchi sifatida e’lon qilingan:
Bu benchmark natijasini tashqarida kuzatiladigan joyga yozadi. Natijada kompilyator butun hisoblashni foydalanilmayotgan kod deb olib tashlash ehtimoli kamayadi.
Bu misoldagi asosiy qoida shuki, benchmark ichidagi asosiy amal b.N marta bajariladi.
2. Stringlarni + bilan birlashtirish sarfi
Bu misolda bir nechta stringni + operatori bilan birlashtirish benchmark qilinadi.
package benchmark
import "testing"
var textResult string
func BenchmarkStringPlus(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
textResult = "Go" + " " + "dasturlash" + " " + "tili"
}
}
Bu yerda:
benchmark natijasiga allocation ma’lumotlarini qo‘shadi.
go test -bench=. -benchmem buyrug‘i natijada bitta amalga o‘rtacha nechta allocation to‘g‘ri kelganini ko‘rsatadi.
Lekin bu misolda nozik bir holat bor.
Birlashtirilayotgan qismlarning hammasi constant stringlar:
Kompilyator bunday ifodani compile time’da oldindan birlashtirishi mumkin.
Ya’ni yozuv manba kodda:
ko‘rinishida turgan bo‘lsa ham, runtime’da har safar alohida string concatenation bajarilishi shart emas.
Shuning uchun bu benchmark natijasi:
Umuman olganda
+operatori stringlarni birlashtirishda doim shuncha allocation qiladi.
degan umumiy xulosani bermaydi.
U faqat aynan shu kod shaklining xatti-harakatini o‘lchaydi.
3. strings.Builder uchun sig‘im ajratish
Bu misolda strings.Builder yordamida string quriladi.
Builder’ga kerak bo‘ladigan sig‘im Grow() orqali oldindan beriladi.
package benchmark
import (
"strings"
"testing"
)
var builderResult string
func BenchmarkBuilder(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()
}
}
Avval bo‘sh builder yaratiladi:
Keyin:
chaqiriladi.
Yakuniy string:
bo‘ladi.
Uning uzunligi 18 bayt.
Shuning uchun builder’ga oldindan kamida 18 bayt sig‘im kerakligi aytilmoqda.
Keyin ikki qism yoziladi:
Agar oldindan ajratilgan sig‘im yetarli bo‘lsa, builder buffer’ni kattalashtirish uchun qo‘shimcha allocation qilishga muhtoj bo‘lmasligi mumkin.
Yakuniy string:
orqali olinadi.
Bu misol Grow()ning asosiy maqsadini ko‘rsatadi: kerakli hajm oldindan taxmin qilinsa, buffer o‘sishidagi ortiqcha allocationlarni kamaytirish mumkin.
4. Setup vaqtini ResetTimer() bilan chiqarib tashlash
Bu misolda benchmarkdan oldin slice tayyorlanadi.
Slice yaratish va uni to‘ldirish benchmark qilinayotgan asosiy amal emas. Shu sabab bu vaqt o‘lchovdan chiqarib tashlanadi.
package benchmark
import "testing"
var lastValue int
func BenchmarkLastValue(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]
}
}
Avval:
orqali uzunligi 1000 bo‘lgan slice yaratiladi.
Uning valid indekslari:
bo‘ladi.
Keyin slice indeks qiymatlari bilan to‘ldiriladi:
Shundan keyin:
chaqiriladi.
Bu nuqtadan oldingi setup benchmark vaqtiga kiritilmaydi.
Asosiy benchmark amalida:
ishlatiladi.
len(numbers) qiymati:
Shuning uchun:
hosil bo‘ladi.
Demak, oxirgi element olinadi.
Bu misoldagi asosiy qoida — benchmarkdan tashqari tayyorlash xarajatini ResetTimer() yordamida ajratish.
5. Tayyorlashni vaqtincha to‘xtatish
Bu misolda timer qo‘lda to‘xtatiladi va keyin yana yoqiladi.
package benchmark
import "testing"
var lookupResult bool
func BenchmarkLookup(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"]
}
}
Avval:
timer’ni vaqtincha to‘xtatadi.
Keyin map yaratiladi:
Bu misolda map yaratish o‘lchanayotgan amal emas.
Shuning uchun map tayyor bo‘lgach:
bilan benchmark timer’i qayta ishga tushiriladi.
Sikl ichida esa:
map lookup bajariladi.
Map lookup ikki qiymat qaytarishi mumkin:
Bu misolda qiymatning o‘zi kerak emas. Shu sabab birinchi natija _ bilan tashlab yuborilgan.
lookupResult esa kalit topilgan yoki topilmaganini saqlaydi.
Bu map’da "go" mavjud, shuning uchun u true bo‘ladi.
Bu misol setup va asosiy benchmark amalini timer orqali ajratishni ko‘rsatadi.
6. Kirish hajmini SetBytes() bilan ko‘rsatish
Ba’zi benchmarklarda faqat bitta amal qancha vaqt olgani emas, qancha ma’lumot qayta ishlangani ham muhim.
SetBytes() shunday holatlarda ishlatiladi.
package benchmark
import (
"strings"
"testing"
)
var containsResult bool
func BenchmarkContains(b *testing.B) {
text := strings.Repeat("a", 1024)
b.SetBytes(int64(len(text)))
for i := 0; i < b.N; i++ {
containsResult = strings.Contains(text, "z")
}
}
Avval:
orqali 1024 ta a belgidan iborat string hosil qilinadi.
Bu belgilar ASCII bo‘lgani uchun har biri bir bayt egallaydi.
Demak, string hajmi:
bo‘ladi.
1024 bayt:
ga teng.
Keyin:
benchmark runner’ga har bir operatsiyada taxminan nechta bayt qayta ishlanayotganini bildiradi.
Asosiy amal:
Matnda z mavjud emas.
Shu sabab qidiruv mos keluvchi belgini topmaydi va ushbu kirish uchun string bo‘ylab qidirishni davom ettiradi.
SetBytes() benchmark natijasida throughput, masalan MB/s, ko‘rsatkichini hisoblashga imkon beradi.
Bu ayniqsa parsing, encoding, hashing yoki katta buffer bilan ishlaydigan kodlar uchun foydali.
7. Maxsus ko‘rsatkich chiqarish
Go benchmarklari faqat standart ns/op yoki allocation ko‘rsatkichlari bilan cheklanmaydi.
ReportMetric() orqali o‘zingizga kerakli metric’ni ham chiqarishingiz mumkin.
package benchmark
import "testing"
var productResult int
func BenchmarkProduct(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
}
}
Bu yerda slice:
to‘rtta elementdan iborat.
Shuning uchun:
natijasi:
bo‘ladi.
Keyin:
benchmark natijasiga:
ma’nosidagi metric qo‘shadi.
Ko‘paytma esa:
dan boshlanadi.
Sababi:
1 ko‘paytirish uchun neytral qiymatdir.
Hisoblash bosqichlari:
Yakuniy natija:
bo‘ladi.
Bu misol benchmarkga domen uchun foydali bo‘lgan o‘z metric’ingizni qo‘shish mumkinligini ko‘rsatadi.
8. Benchmarkni bir necha marta takrorlash
Bitta benchmark natijasi tasodifan odatdagidan tez yoki sekin chiqishi mumkin.
Shuning uchun performance taqqoslashda benchmarkni bir necha marta ishga tushirish foydali.
package benchmark
import "testing"
var compareResult int
func BenchmarkCalculation(b *testing.B) {
a, multiplier, extra := 25, 4, 10
for i := 0; i < b.N; i++ {
compareResult = (a * multiplier) + extra
}
}
Bu misolda hisoblash:
bajariladi.
Bosqichma-bosqich:
Lekin bu misoldagi asosiy fikr hisoblashning o‘zi emas. Asosiy maqsad benchmarkni bir necha marta takrorlashni ko‘rsatishdir.
Haqiqiy _test.go benchmarkini quyidagicha besh marta bajarish mumkin:
Bu yerda:
benchmark run’ini besh marta takrorlaydi.
Nega bu foydali?
Chunki bitta natija vaqtinchalik omillar ta’sirida chiqishi mumkin:
- boshqa process CPU ishlatayotgan bo‘lishi mumkin;
- scheduler boshqacha taqsimlagan bo‘lishi mumkin;
- CPU frequency o‘zgargan bo‘lishi mumkin;
- cache holati farq qilishi mumkin.
Bir nechta o‘lchov natijaning tarqalishini ko‘rishga yordam beradi.
Lekin taqqoslanayotgan benchmarklar imkon qadar bir xil muhitda bajarilishi kerak.
Concurrent koddagi data racelarni aniqlash Race detector darsida alohida ko‘rib chiqiladi.