GoDasturchi
Where Memory Lives

Where Memory Lives

Xotira qayerda yashaydi

Har bir ishlayotgan Go dasturi yaratadigan qiymatlarni xotiraning ikkita mintaqasidan birida saqlaydi, va qiymat qaysi biriga tushishi uning qancha yashashini va kim uni tozalashini belgilaydi.

Stack — bitta funksiya chaqiruviga bog'liq tez, avtomatik xotira: u chaqiruv boshlanganda paydo bo'ladi va chaqiruv qaytganda yo'qoladi. Heap esa chaqiruv tugagandan keyin ham qolishi kerak bo'lgan qiymatlar uchun sekinroq, umumiy xotira, uni fon rejimidagi axlat yig'uvchi (garbage collector) tozalaydi. Aksariyat qiymatlar stackka tushadi; heap esa chaqiruvidan uzoqroq yashashi kerak bo'lganlar uchun istisno.

Stack

Stack hozir ishlab turgan chaqiruvlarning lokal o'zgaruvchilarini, har bir goroutine uchun bittadan, ushlab turadi. Funksiya chaqirilganda, Go u uchun frame deb ataladigan, aynan o'sha chaqiruvning lokal o'zgaruvchilarini sig'diradigan xotira blokini ajratadi. Frame chaqiruv ishlab turgan davomida mavjud bo'ladi va chaqiruv qaytgan zahoti qaytarib beriladi.

Bu stack deb atalishining sababi — freymlar chaqiruv tartibida, tarelkalar to'plami kabi ustma-ust to'planadi. Agar handleRequest parseBodyni chaqirsa, parseBodyning freymi handleRequestnikining ustiga qo'yiladi. Eng ustidagi chaqiruv har doim keyingi qaytadigani bo'lgani uchun, freymlar faqat ustidan, qo'shilgan tartibning teskarisida olib tashlanadi.

Aynan shu tartib stackni tez qiladi. Go stackning tepasi qayerda ekanini kuzatib boradi, va freymni bo'shatish shunchaki o'sha belgini undan pastga surish degani. Skanerlanadigan obyektlar ro'yxati yo'q va qidirib topib qaytarib olinadigan xotira yo'q, shuning uchun qaytayotgan funksiya deyarli bepul o'zi ortidan tozalaydi. Aynan shu tezlik stackni standart qiladi.

Heap

Heap — o'zini yaratgan chaqiruvdan uzoqroq yashashi kerak bo'lgan qiymatlar uchun alohida, uzoqroq yashovchi havza. Lokalga pointerni qaytaring, u freymda qololmaydi, chunki pointer hali ishlatilayotganda funksiya qaytgan zahoti freym yo'qoladi, shuning uchun qiymat buning o'rniga heapga tushadi. Uni hech narsa avtomatik ozod qilmaydi; Go'ning axlat yig'uvchisi vaqti-vaqti bilan ishga tushib, hech kim ishora qilmayotgan narsani topadi va uni qaytarib oladi. Bu supurish haqiqiy protsessor ishini talab qiladi, bu esa heap'ning chaqiruvdan uzoqroq yashashi uchun to'laydigan narxi.

Shunday qilib, har bir qiymat uchun savol bor: stackmi yoki heapmi? Shunga qarang:

example.go
func newCounter() *Counter {
	counter := Counter{}
	return &counter
}

C'da bu xato bo'lardi. Lokal counter newCounterning stack freymida yashaydi, shuning uchun funksiya qaytgan zahoti, o'sha freym yo'qoladi va siz qaytargan manzil qaytarib olingan xotiraga ishora qiladi.

Go'da bu shunchaki ishlaydi. Kompilyator kodingizni o'qiydi, &counter funksiyadan uzoqroq yashashini payqaydi, va jimgina counterni heapga joylashtiradi. Bu stack-yoki-heap qarori escape analysis (chiqib ketish tahlili) deb ataladi, va kompilyator uni siz yozgan har bir qiymat uchun ishga tushiradi.

Kompilyator qanday tanlaydi

Yaratgan har bir qiymat uchun, kompilyator ikkita tekshiruv o'tkazadi.

  • Manzili hech qachon olinganmi? Agar &value hech qachon uchramasa, qiymat stackda qoladi.
  • Pointer freymdan uzoqroq yashaydimi? Kompilyator pointerning har bir ishlatilishini kuzatadi. Agar u funksiya ichida qolsa — stack. Agar hech bo'lmasa bitta yo'l uni chiqib ketishiga imkon bersa — heap.

Yana bir narsa qarorni o'zgartirishi mumkin: inlining. Funksiya kichik bo'lganda, kompilyator ko'pincha chaqiruvni butunlay o'tkazib yuboradi va uning tanasini to'g'ridan-to'g'ri chaqiruvchiga joylashtiradi. newCounterning tanasi mainga joylashtirilgandan so'ng, pointer chiqib ketishi mumkin bo'lgan alohida newCounter freymi umuman qolmaydi. &Counter{} endi to'g'ridan-to'g'ri main ichida yaratiladi, shuning uchun escape analysis endi uning newCounterdan emas, maindan uzoqroq yashashini so'raydi. newCounterdan uzoqroq yashash uchun heapga borishi kerak bo'lgan qiymat, shu chegara yo'qolgach, mainning stackida qolishi mumkin.

Qiymatni heapga nima yuboradi

Deyarli har doim, bu quyidagi to'rtta andazadan birontasidan keladi:

  • Pointerni funksiyadan qaytarish
  • Uni chaqiruvdan uzoqroq yashaydigan yopilish (closure)ga ushlab olish (go func(), yoki siz qaytaradigan yopilish)
  • Uni freymdan chiqib ketadigan slice, map, kanal, yoki interfeysda saqlash
  • Uni kompilyator ko'ra olmaydigan interfeys metodi yoki funksiya-pointer argumenti orqali uzatish

Shubha bo'lganda, kompilyator heapni tanlaydi. Stack tomon noto'g'ri qaror pointerlarga qaytarib olingan xotirani ko'rsatish imkonini bergan bo'lardi. Heap tomon noto'g'ri qaror esa shunchaki GC uchun ko'proq ish yaratadi.

Masalan, n := 10, return &User{...}, go func() { upload(payload) }(), va cache = append(cache, item) kabi qiymatlarning barchasi escape analysis (inlining'dan keyin) orqali o'tadi: ishora freym ichida qolsa — stack (funksiya qaytganda ozod qilinadi), pointer freymdan uzoqroq yashasa — heap (hech kim ishora qilmay qolganda GC qaytarib oladi).

Qachon e'tibor berish kerak

Deyarli hech qachon. Idiomatik Go yozing, tanlashni kompilyatorga qoldiring, tozalashni GC'ga qoldiring. Faqat qizigan yo'lni (hot path) o'lchaganingizda va profilda ajratmalar (allocations) ko'rinayotganda escape qarorlarini quvlang. Bir marta ishga tushadigan CLI yoki daqiqasiga o'n marta chaqiriladigan ishlovchi hech kim sezmasdan erkin heap-ajratishi mumkin. Sekundiga o'n ming marta ishlaydigan so'rov ishlovchisi esa — tasodifiy heap trafigi kechikish tailarida ko'rina boshlaydigan joy.

Qaysi qator ajratishini taxmin qilish o'rniga benchmarkdan boshlang. go test -bench=. -benchmem tekshirilayotgan kod uchun allocs/opni xabar qiladi, va qizigan yo'ldagi yuqori son quvlashga arziydigan signal. Shu darsning boshidagi newCounterni benchmark qiling:

example.go
func BenchmarkNewCounter(b *testing.B) {
	for b.Loop() {
		_ = newCounter()
	}
}
example.text
BenchmarkNewCounter-8    38500102    31.2 ns/op    8 B/op    1 allocs/op

O'sha 1 allocs/op — escape analysis hal qilgan heap ajratmasi: newCounter &counterni qaytaradi, shuning uchun qiymat freymda qola olmaydi. Benchmark qizigan, ajratma qiladigan yo'lni ko'rsatgach, kompilyatordan qaysi qator ekanini so'rang:

example.bash
go build -gcflags="-m" main.go

U har bir qaror uchun bitta qator chop etadi. Eng ko'p ko'radigan uchta shakl:

example.text
./user.go:12:9: &User{...} escapes to heap
./user.go:18:6: moved to heap: x
./user.go:22:9: &User{...} does not escape

Birinchisi — pointeri freymni tark etadigan composite literal. Ikkinchisi — manzili funksiyadan uzoqroq yashagan nomlangan lokal. Uchinchisi — siz izlayotgan hukm.

Ikkita bir xil ajratma, ikkita hukm

example.go
package main

type LogEntry struct {
	Level   string
	Message string
}

// Pointerni qaytaradi, shuning uchun chaqiruvchi LogEntry'ni shu chaqiruvdan keyin ham tirik saqlaydi.
func newLogEntry(level, msg string) *LogEntry {
	return &LogEntry{Level: level, Message: msg}
}

// Satr yasaydi va pointerni tashlab yuboradi.
func formatLogLine(level, msg string) string {
	e := &LogEntry{Level: level, Message: msg}
	return "[" + e.Level + "] " + e.Message
}

func main() {
	_ = newLogEntry("info", "request handled")
	_ = formatLogLine("warn", "slow query")
}

go build -gcflags="-m" shu faylda (qisqartirilgan) chop etadi:

example.text
./main.go:10:9: &LogEntry{...} escapes to heap
./main.go:15:7: &LogEntry{...} does not escape

Bir xil &LogEntry{...} sintaksisi, qarama-qarshi hukmlar. newLogEntry pointerni qaytaradi, shuning uchun chaqiruvchi uni freymdan keyin ham tirik saqlaydi. formatLogLine esa undan faqat maydonlarni o'qiydi va satr qaytaradi, shuning uchun pointer funksiyaning o'zidan oldin o'ladi.

Key Takeaway

Key Takeaway:

Go escape analysis orqali har bir qiymat uchun stack yoki heapni tanlaydi. Kompilyator pointer freym bilan birga o'lishini isbotlay olmasa, heapni tanlaydi, va axlat yig'uvchi hech kim qiymatga ishora qilmay qolgach heapni qaytarib oladi.

Buni standart bo'yicha Go'ga qoldiring, va faqat qizigan yo'ldagi benchmark ajratmalar haqiqatan xarajat qilayotganini ko'rsatganda escape analysis'ga murojaat qiling.

NEXT UP

Helping the Compiler

Where Memory Lives

Xotira qayerda yashaydi

Har bir ishlayotgan Go dasturi yaratadigan qiymatlarni xotiraning ikkita mintaqasidan birida saqlaydi, va qiymat qaysi biriga tushishi uning qancha yashashini va kim uni tozalashini belgilaydi.

Stack — bitta funksiya chaqiruviga bog'liq tez, avtomatik xotira: u chaqiruv boshlanganda paydo bo'ladi va chaqiruv qaytganda yo'qoladi. Heap esa chaqiruv tugagandan keyin ham qolishi kerak bo'lgan qiymatlar uchun sekinroq, umumiy xotira, uni fon rejimidagi axlat yig'uvchi (garbage collector) tozalaydi. Aksariyat qiymatlar stackka tushadi; heap esa chaqiruvidan uzoqroq yashashi kerak bo'lganlar uchun istisno.

Stack

Stack hozir ishlab turgan chaqiruvlarning lokal o'zgaruvchilarini, har bir goroutine uchun bittadan, ushlab turadi. Funksiya chaqirilganda, Go u uchun frame deb ataladigan, aynan o'sha chaqiruvning lokal o'zgaruvchilarini sig'diradigan xotira blokini ajratadi. Frame chaqiruv ishlab turgan davomida mavjud bo'ladi va chaqiruv qaytgan zahoti qaytarib beriladi.

Bu stack deb atalishining sababi — freymlar chaqiruv tartibida, tarelkalar to'plami kabi ustma-ust to'planadi. Agar handleRequest parseBodyni chaqirsa, parseBodyning freymi handleRequestnikining ustiga qo'yiladi. Eng ustidagi chaqiruv har doim keyingi qaytadigani bo'lgani uchun, freymlar faqat ustidan, qo'shilgan tartibning teskarisida olib tashlanadi.

Aynan shu tartib stackni tez qiladi. Go stackning tepasi qayerda ekanini kuzatib boradi, va freymni bo'shatish shunchaki o'sha belgini undan pastga surish degani. Skanerlanadigan obyektlar ro'yxati yo'q va qidirib topib qaytarib olinadigan xotira yo'q, shuning uchun qaytayotgan funksiya deyarli bepul o'zi ortidan tozalaydi. Aynan shu tezlik stackni standart qiladi.

Heap

Heap — o'zini yaratgan chaqiruvdan uzoqroq yashashi kerak bo'lgan qiymatlar uchun alohida, uzoqroq yashovchi havza. Lokalga pointerni qaytaring, u freymda qololmaydi, chunki pointer hali ishlatilayotganda funksiya qaytgan zahoti freym yo'qoladi, shuning uchun qiymat buning o'rniga heapga tushadi. Uni hech narsa avtomatik ozod qilmaydi; Go'ning axlat yig'uvchisi vaqti-vaqti bilan ishga tushib, hech kim ishora qilmayotgan narsani topadi va uni qaytarib oladi. Bu supurish haqiqiy protsessor ishini talab qiladi, bu esa heap'ning chaqiruvdan uzoqroq yashashi uchun to'laydigan narxi.

Shunday qilib, har bir qiymat uchun savol bor: stackmi yoki heapmi? Shunga qarang:

example.go
func newCounter() *Counter {
	counter := Counter{}
	return &counter
}

C'da bu xato bo'lardi. Lokal counter newCounterning stack freymida yashaydi, shuning uchun funksiya qaytgan zahoti, o'sha freym yo'qoladi va siz qaytargan manzil qaytarib olingan xotiraga ishora qiladi.

Go'da bu shunchaki ishlaydi. Kompilyator kodingizni o'qiydi, &counter funksiyadan uzoqroq yashashini payqaydi, va jimgina counterni heapga joylashtiradi. Bu stack-yoki-heap qarori escape analysis (chiqib ketish tahlili) deb ataladi, va kompilyator uni siz yozgan har bir qiymat uchun ishga tushiradi.

Kompilyator qanday tanlaydi

Yaratgan har bir qiymat uchun, kompilyator ikkita tekshiruv o'tkazadi.

  • Manzili hech qachon olinganmi? Agar &value hech qachon uchramasa, qiymat stackda qoladi.
  • Pointer freymdan uzoqroq yashaydimi? Kompilyator pointerning har bir ishlatilishini kuzatadi. Agar u funksiya ichida qolsa — stack. Agar hech bo'lmasa bitta yo'l uni chiqib ketishiga imkon bersa — heap.

Yana bir narsa qarorni o'zgartirishi mumkin: inlining. Funksiya kichik bo'lganda, kompilyator ko'pincha chaqiruvni butunlay o'tkazib yuboradi va uning tanasini to'g'ridan-to'g'ri chaqiruvchiga joylashtiradi. newCounterning tanasi mainga joylashtirilgandan so'ng, pointer chiqib ketishi mumkin bo'lgan alohida newCounter freymi umuman qolmaydi. &Counter{} endi to'g'ridan-to'g'ri main ichida yaratiladi, shuning uchun escape analysis endi uning newCounterdan emas, maindan uzoqroq yashashini so'raydi. newCounterdan uzoqroq yashash uchun heapga borishi kerak bo'lgan qiymat, shu chegara yo'qolgach, mainning stackida qolishi mumkin.

Qiymatni heapga nima yuboradi

Deyarli har doim, bu quyidagi to'rtta andazadan birontasidan keladi:

  • Pointerni funksiyadan qaytarish
  • Uni chaqiruvdan uzoqroq yashaydigan yopilish (closure)ga ushlab olish (go func(), yoki siz qaytaradigan yopilish)
  • Uni freymdan chiqib ketadigan slice, map, kanal, yoki interfeysda saqlash
  • Uni kompilyator ko'ra olmaydigan interfeys metodi yoki funksiya-pointer argumenti orqali uzatish

Shubha bo'lganda, kompilyator heapni tanlaydi. Stack tomon noto'g'ri qaror pointerlarga qaytarib olingan xotirani ko'rsatish imkonini bergan bo'lardi. Heap tomon noto'g'ri qaror esa shunchaki GC uchun ko'proq ish yaratadi.

Masalan, n := 10, return &User{...}, go func() { upload(payload) }(), va cache = append(cache, item) kabi qiymatlarning barchasi escape analysis (inlining'dan keyin) orqali o'tadi: ishora freym ichida qolsa — stack (funksiya qaytganda ozod qilinadi), pointer freymdan uzoqroq yashasa — heap (hech kim ishora qilmay qolganda GC qaytarib oladi).

Qachon e'tibor berish kerak

Deyarli hech qachon. Idiomatik Go yozing, tanlashni kompilyatorga qoldiring, tozalashni GC'ga qoldiring. Faqat qizigan yo'lni (hot path) o'lchaganingizda va profilda ajratmalar (allocations) ko'rinayotganda escape qarorlarini quvlang. Bir marta ishga tushadigan CLI yoki daqiqasiga o'n marta chaqiriladigan ishlovchi hech kim sezmasdan erkin heap-ajratishi mumkin. Sekundiga o'n ming marta ishlaydigan so'rov ishlovchisi esa — tasodifiy heap trafigi kechikish tailarida ko'rina boshlaydigan joy.

Qaysi qator ajratishini taxmin qilish o'rniga benchmarkdan boshlang. go test -bench=. -benchmem tekshirilayotgan kod uchun allocs/opni xabar qiladi, va qizigan yo'ldagi yuqori son quvlashga arziydigan signal. Shu darsning boshidagi newCounterni benchmark qiling:

example.go
func BenchmarkNewCounter(b *testing.B) {
	for b.Loop() {
		_ = newCounter()
	}
}
example.text
BenchmarkNewCounter-8    38500102    31.2 ns/op    8 B/op    1 allocs/op

O'sha 1 allocs/op — escape analysis hal qilgan heap ajratmasi: newCounter &counterni qaytaradi, shuning uchun qiymat freymda qola olmaydi. Benchmark qizigan, ajratma qiladigan yo'lni ko'rsatgach, kompilyatordan qaysi qator ekanini so'rang:

example.bash
go build -gcflags="-m" main.go

U har bir qaror uchun bitta qator chop etadi. Eng ko'p ko'radigan uchta shakl:

example.text
./user.go:12:9: &User{...} escapes to heap
./user.go:18:6: moved to heap: x
./user.go:22:9: &User{...} does not escape

Birinchisi — pointeri freymni tark etadigan composite literal. Ikkinchisi — manzili funksiyadan uzoqroq yashagan nomlangan lokal. Uchinchisi — siz izlayotgan hukm.

Ikkita bir xil ajratma, ikkita hukm

example.go
package main

type LogEntry struct {
	Level   string
	Message string
}

// Pointerni qaytaradi, shuning uchun chaqiruvchi LogEntry'ni shu chaqiruvdan keyin ham tirik saqlaydi.
func newLogEntry(level, msg string) *LogEntry {
	return &LogEntry{Level: level, Message: msg}
}

// Satr yasaydi va pointerni tashlab yuboradi.
func formatLogLine(level, msg string) string {
	e := &LogEntry{Level: level, Message: msg}
	return "[" + e.Level + "] " + e.Message
}

func main() {
	_ = newLogEntry("info", "request handled")
	_ = formatLogLine("warn", "slow query")
}

go build -gcflags="-m" shu faylda (qisqartirilgan) chop etadi:

example.text
./main.go:10:9: &LogEntry{...} escapes to heap
./main.go:15:7: &LogEntry{...} does not escape

Bir xil &LogEntry{...} sintaksisi, qarama-qarshi hukmlar. newLogEntry pointerni qaytaradi, shuning uchun chaqiruvchi uni freymdan keyin ham tirik saqlaydi. formatLogLine esa undan faqat maydonlarni o'qiydi va satr qaytaradi, shuning uchun pointer funksiyaning o'zidan oldin o'ladi.

Key Takeaway

Key Takeaway:

Go escape analysis orqali har bir qiymat uchun stack yoki heapni tanlaydi. Kompilyator pointer freym bilan birga o'lishini isbotlay olmasa, heapni tanlaydi, va axlat yig'uvchi hech kim qiymatga ishora qilmay qolgach heapni qaytarib oladi.

Buni standart bo'yicha Go'ga qoldiring, va faqat qizigan yo'ldagi benchmark ajratmalar haqiqatan xarajat qilayotganini ko'rsatganda escape analysis'ga murojaat qiling.

NEXT UP

Helping the Compiler