Dasturlashda unumdorlik muammolarini noto'g'ri "yechish"ning klassik ssenariysi bor: kimdir kodni ko'zdan kechiradi, nima sekin ishlayotgani haqida o'zicha xulosa yasaydi, o'sha joyni o'zgartiradi va o'zgarish nazariy jihatdan to'g'ridek tuyulgani sababli g'alaba e'lon qiladi. Ba'zan bu tasodifan yordam berishi ham mumkin. Ammo ko'pincha aksincha bo'ladi, chunki garbage collector, optimallashtiruvchi kompilyator va work-stealing scheduler bilan ishlaydigan zamonaviy tilda dasturchining quruq sezgisi (intuitsiyasi) ko'pincha aldamchi bo'lib chiqadi: sekin ko'ringan kod qismi aslida muammo bo'lmasligi, tizim vaqtini yutib yuborayotgan haqiqiy to'siq (bottleneck) esa umuman kutilmagan joyda yotgan bo'lishi mumkin. Goning profiling vositalari ana shu taxminlarni aniq o'lchovlar bilan almashtirish uchun yaratilgan bo'lib, ushbu bob ulardan to'g'ri foydalanish — kodga tegmasdan avval vaqt va xotira aslida qayerga sarflanayotganini aniqlash haqidadir.
Buni qayta-qayta ta'kidlash zarurligining sababi shuki, xatoga yo'l qo'yish juda oson. O'zingizga tanish kodni optimallashtirish yoqimli tuyuladi va nima uchun u sekin ishlayotgani haqida ishonarli nazariyalar to'qish qiyin emas. Profiling esa taxminlarni aniq raqamlar bilan almashtiradi: haqiqiy dasturingizning real ishga tushirilishida qaysi funksiya CPU vaqtining yoki xotiraning qancha qismini egallayotganini ko'rsatadi. Buni tushunish dasturchi ishini butunlay o'zgartiradi:
Profiling taxminni raqam bilan almashtiradi: qaysi funksiya CPU vaqtining yoki baytlarning qancha qismini tashkil qildi.
Goning asosiy profileri bo'lgan runtime/pprof standart kutubxona tarkibiga kiradi va uni ishlatishning ikkita asosiy idiomatik usuli mavjud.
Uzoq ishlaydigan tarmoq servislari (HTTP serverlar) uchun standart mux'da profiling endpoint'larini ro'yxatdan o'tkazish maqsadida net/http/pprof paketini bo'sh import orqali ulang:
import (
"log"
"net/http"
_ "net/http/pprof"
)
func main() {
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
runApplication()
}
Dastur ishlayotgan paytda go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30 buyrug'i jonli jarayondan 30 soniyalik CPU namunalarini yig'ib oladi. Bu juda qulay, chunki serverni qayta ishga tushirmasdan, bevosita real yuklama ostida profil olish imkonini beradi. Biroq production muhitida bu endpoint'larni faqat ichki tarmoqqa (internal listener) bog'lash yoki kuchli autentifikatsiya bilan himoyalash shart, chunki bu endpointlar begonalarga ko'rinmasligi lozim bo'lgan goroutinelar steki va heap xotirasi tafsilotlarini fosh qilib qo'yishi mumkin. Shuningdek, /debug/pprof/heap, /debug/pprof/goroutine, /debug/pprof/block va /debug/pprof/mutex orqali boshqa turdagi profillarni ham olish mumkin (oxirgi ikkitasi uchun runtime.SetBlockProfileRate va runtime.SetMutexProfileFraction orqali kuzatuv chastotasini yoqish talab etiladi).
Benchmarklar yoki bir martalik skriptlar uchun esa natijalarni to'g'ridan-to'g'ri faylga yozib oling:
cpuFile, _ := os.Create("cpu.prof")
defer cpuFile.Close()
pprof.StartCPUProfile(cpuFile)
defer pprof.StopCPUProfile()
runApplication()
Yoki undan ham osonroq yo'li — go test vositasiga topshiring: go test -bench=. -cpuprofile=cpu.prof -memprofile=mem.prof buyrug'i benchmarklarni ishga tushiradi va ikkala profilni kodga hech qanday o'zgartirish kiritmasdan faylga saqlaydi.
Faqat real production yuklamasi ostida vaqti-vaqti bilan yuzaga chiqadigan, ammo benchmarklarda qaytarilishi qiyin bo'lgan muammolar (masalan, kutilmagan latency kechikishlari yoki vaqti-vaqti bilan yuz beradigan to'xtashlar) uchun Go 1.25 da qo'shilgan runtime/trace.FlightRecorder vositasi ayni muddao: u xotirada so'nggi bir necha soniyalik ijro jarayonini juda past qo'shimcha xarajat bilan aylanuvchi buferda saqlab turadi. Muayyan muammo signali (masalan, kechikish me'yordan oshishi yoki alert tushishi) paydo bo'lganda FlightRecorder.WriteTo orqali o'sha lahzagacha bo'lgan to'liq ijro izini (execution trace) darhol faylga tushirib olish mumkin.
Profil fayli yaratilgach, go tool pprof -http=:8080 cpu.prof buyrug'i pprof'ning qulay veb interfeysini ochadi. Go 1.26 dan boshlab u sukut bo'yicha flame graph (alanga grafigi) ko'rinishida ochiladi — bu qaysi funksiya eng ko'p sof vaqt sarflayotganini bir qarashda aniqlashning eng qulay vizual usulidir.
go tool pprof cpu.prof buyrug'i sizni interaktiv terminal rejimiga tushiradi. top10 buyrug'i eng ko'p resurs yegan 10 ta funksiyani ko'rsatadi, bunda ikkita ustunni to'g'ri farqlash muhim:
flat — funksiyaning o'z kodida (u chaqirgan boshqa funksiyalarni hisobga olmaganda) sarflangan sof vaqt;cum (cumulative) — ushbu funksiya va u chaqirgan barcha quyi funksiyalarda sarflangan umumiy vaqt.Yuqori flat ko'rsatkichiga ega funksiya o'z ichida og'ir hisob-kitoblarni bajarayotgan bo'ladi: qattiq sikllar, og'ir satr formatlash yoki samarasiz algoritmlar. Yuqori cum, ammo past flatga ega funksiya esa koordinator vazifasini bajaradi, asosiy vaqt esa u chaqirayotgan quyi funksiyalarga ketayotgan bo'ladi. list funksiyaNomi buyrug'i esa kod qatorlari bo'yicha vaqt qayerga ketganini batafsil ko'rsatib beradi. Brauzerdagi flame graph esa bu iyerarxiyani vizual bloklar kengligi orqali o'ta tushunarli tarzda taqdim etadi.
Xotira profillari xuddi shu vositadan foydalanadi, ammo ikki xil rejimga ega: -alloc_space dastur boshlanganidan buyon qayerda qancha umumiy xotira ajratilganini ko'rsatadi (bu garbage collector bosimi qayerdan kelayotganini topishda asqotadi), -inuse_space esa ayni damda xotirada tirik saqlanib turgan baytlar hajmini ko'rsatadi (xotira sizib chiqishlarini topish uchun kerak). Ushbu ikki ko'rsatkichni adashtirish yangi boshlovchilar orasida juda keng tarqalgan: funksiya -alloc_space bo'yicha eng yuqorida turishi, ammo -inuse_spaceda deyarli ko'rinmasligi mumkin. Bu xotira sizib chiqishi (leak) emas, balki GC'ga haddan tashqari yuklama (GC pressure) muammosidir va uni pooling yoki slayslarni oldindan o'lchash orqali hal qilish lozim.
CPU-bog'liq muammolar vaqtni hisob-kitoblarga sarflaydi: ichma-ich sikllar, qayta-qayta bajariladigan ortiqcha hisoblar, samarasiz asimptotik murakkablik. Yechim deyarli doimo algoritmik xarakterga ega: kvadratik siklni chiziqli algoritmga o'tkazish, takroriy hisob-kitoblarni memoizatsiya qilish yoki mustaqil vazifalarni bo'sh protsessor yadrolari bo'ylab parallellashtirish.
// O(n²): har bir element uchun butun slice'ni qayta skanerlaydi
func slowSum(numbers []int) int {
sum := 0
for i := range numbers {
for j := range numbers {
if i != j {
sum += numbers[i] * numbers[j]
}
}
}
return sum
}
// O(n): umumiy yig'indini bir marta oldindan hisoblab, qolganini undan chiqaradi
func fastSum(numbers []int) int {
total := 0
for _, n := range numbers {
total += n
}
sum := 0
for _, n := range numbers {
sum += n * (total - n)
}
return sum
}
Xotira-bog'liq muammolar esa vaqtni doimiy xotira ajratish, nusxalash va garbage collector to'xtashlariga sarflaydi. CPU profilida runtime.mallocgc yoki runtime.gcBgMarkWorker eng yuqori o'rinlarda ko'rinsa, bu zudlik bilan heap profiliga o'tish kerakligidan dalolat beruvchi jiddiy signaldir. Bu yerdagi asosiy yechimlar:
make([]T, 0, n)), toki append tufayli xotira qayta-qayta kengayib nusxalanmasin.sync.Pooldan foydalanish.string va []byte o'rtasidagi keraksiz konvertatsiyalardan qochish.var bufPool = sync.Pool{
New: func() any { return new(bytes.Buffer) },
}
func render(w io.Writer, data []byte) error {
buf := bufPool.Get().(*bytes.Buffer)
buf.Reset()
defer bufPool.Put(buf)
buf.Write(data)
_, err := w.Write(buf.Bytes())
return err
}
Go 1.26 dagi Green Tea GC yaxshilanishlari GC xarajatlarini 10–40% ga kamaytirdi, Go 1.27 esa kichik obyektlarni ajratishni taxminan 30% tezlashtirdi. Biroq bu yangilanishlar pooling va oldindan o'lchash kabi eng yaxshi amaliyotlarning ahamiyatini kamaytirmaydi.
Muhim eslatma: sync.Pool doimiy kesh emas. Har bir GC siklida pool'dagi bo'sh elementlar tozalanishi mumkin. Shuning uchun unga faqat vaqtinchalik xotira obyektlari joylanishi kerak.
Faraz qilaylik, elementlar to'plamini qayta ishlovchi quyidagi funksiya bor va ma'lumot ko'paygani sari u sekinlashmoqda:
func processItems(items []Item) []Result {
results := []Result{}
for _, item := range items {
results = append(results, processItem(item))
}
return results
}
Haqiqiy yuklamaga mos ma'lumotlar bilan olingan CPU profili ikkita muammoni ko'rsatadi: runtime.growsliceda sezilarli vaqt sarflanmoqda (slays hajmi o'sgani sari qayta ajratilib nusxalanmoqda) va asosiy vaqt processItem funksiyasining hisob-kitoblariga ketmoqda. Buni quyidagicha tuzatish mumkin:
func processItems(items []Item) []Result {
results := make([]Result, 0, len(items)) // bitta allokatsiya, log(n) emas
var wg sync.WaitGroup
out := make(chan Result, len(items))
for _, item := range items {
wg.Add(1)
go func(it Item) {
defer wg.Done()
out <- processItem(it)
}(item)
}
go func() {
wg.Wait()
close(out)
}()
for r := range out {
results = append(results, r)
}
return results
}
results hajmini oldindan make([]Result, 0, len(items)) orqali belgilash mutlaqo arzon va growslice xarajatini birdan yo'qotadi. Ishni goroutinelar orqali parallellashtirish ancha katta ta'sir kuchiga ega, ammo u faqat processItem haqiqatan CPU resursini talab qilsa, har bir element bir-biriga bog'liq bo'lmasa va serverda bo'sh protsessor yadrolari yetarli bo'lgandagina foyda keltiradi. O'zgarishdan so'ng natijani tekshirish uchun eski profil bilan yangisini taqqoslash eng yaxshi usuldir: go tool pprof -base before.prof after.prof.
Realistik yuklamalarni sinang. Faqat kichik test ma'lumotlari bilan ishlovchi benchmarklar ishlab chiqarishdagi haqiqiy yuklamani to'g'ri aks ettirmaydi. Staging yoki ishlab chiqarish serveridan olingan real profillar eng to'g'ri yo'lni ko'rsatadi.
Profilni yetarlicha uzoq vaqt yozib oling. Bo'sh turgan serverdan 2-3 soniyalik profil olish statistik jihatdan hech qanday ishonchli ma'lumot bermaydi; yuklama ostida kamida 30 soniyalik profil olish odatiy standart hisoblanadi.
Kodni vaqt bo'yicha kuzatish uchun benchmarklar (testing.B) va benchstat vositasidan foydalaning. go test -bench=. -benchmem orqali operatsiya tezligi va xotira ajratishlari sonini o'lchab boring.
Doimiy amal qilinishi kerak bo'lgan cheklist: avval profil oling, muammoli nuqta aynan qayerdaligini isbotlang, bitta o'zgarish kiriting, qayta o'lchang va shundan so'ng ushbu optimizatsiya kiritilgan kod murakkabligiga arziydimi-yo'qmi qaror qiling. Agar unumdorlik yutug'i arzimagan bo'lsa-yu, kod o'qilishi keskin yomonlashsa, bunday o'zgarishdan voz kechgan ma'qul. Keyingi, yakuniy bobda biz aynan mana shunday uzoq muddatli arxitektura va loyihalash tamoyillarini ko'rib chiqamiz.