Ders 08 / 16
Kaçış Çözümlemesi
Kaçış çözümlemesi bir araç bilgisidir: derleyici bir değeri yığıtta mı öbekte mi ayıracağına karar veriyor ve bunu koşum olmadan bildiriyor. Dört işlevin üçü öbeğe taşınıyor ve aynı işlev küçük bir değişiklikle bu kararını değiştiriyor. Sınırlayıcı ölçüm: bu karar dilin bir sözü değil, derleyicinin sürümüne bağlı bir gerçekleştirim ayrıntısı.
Yayın Güncelleme
Bu derste
- Derleyicinin bir değeri yığıtta mı öbekte mi ayıracağına dair kararını koşum olmadan okur
- Aynı işlevin küçük bir değişiklikle öbeğe taşıma kararını nasıl değiştirdiğini koşturarak gösterir
- Kaçış çözümlemesi kararının dilin bir sözü değil, derleyicinin bir gerçekleştirim ayrıntısı olduğunu ayırt eder
İçindekiler
Profil bir değerin nereye ayrıldığını koşumdan sonra, sayıyla söyledi. Ama o değerin yığıtta mı öbekte mi duracağı kararı koşumdan çok önce veriliyor — kaynak henüz çalıştırılmadan, yalnız derlenirken. Bu ders o kararın kendisine bakıyor: derleyici bu kararı nasıl bildiriyor ve aynı işlevin gövdesinde küçük bir değişiklik bu kararı nasıl değiştiriyor.
Bu, kursun açılışından beri süren kaynak/araç/koşum ayrımının en net araç örneği. Kaçış
çözümlemesi kaynakta yazılı değil — hiçbir yerde öbek ya da yığıt sözcüğü geçmiyor; kod yalnız
bir değişken tanımlıyor ve onu kullanıyor. Ama derleyici bu kararı vermek için programı
çalıştırmıyor da; kararı derleme sırasında veriyor ve isteğe bağlı olarak bildiriyor. Bu,
koşum bilgisinden farklı: koşum bilgisi (bir önceki dersteki profil sayısı gibi) yalnız
çalıştırıldığında ortaya çıkıyordu ve her koşumda aynı kalıp kalmadığı ayrıca sınanıyordu. Araç
bilgisi ise koşum gerektirmiyor — aynı kaynak, aynı derleyiciyle, her seferinde aynı kararı
veriyor.
Yığıt, Öbek ve Aradaki Karar
Bir değerin yığıtta mı öbekte mi duracağı sorusu bu kursta yeni değil: yığıt ve öbek Bilgisayarlar Nasıl Çalışır kursunda (M01/K01) bellek düzeninin bir parçası olarak kuruldu ve burada tekrarlanmıyor. O kursun kurduğu ayrım şuydu — yığıt bir çağrı çerçevesiyle birlikte açılıp kapanan, hızlı ve sıralı bir alan; öbek ise çağrı çerçevesinden bağımsız, elle (ya da bu derste görüleceği gibi derleyicinin kararıyla) yönetilen bir alan. Bu ders o iki alanın ne olduğunu değil, bir değerin hangisine gideceğine kimin karar verdiğini ölçüyor.
Kararın kendisi basit bir kuralla özetlenebilir: bir değerin ömrü, onu oluşturan çağrı çerçevesinin ömrünü aşıyorsa değer öbeğe taşınmak zorunda — aksi hâlde çağrı çerçevesi kapandığında değer de kaybolurdu. Derleyici bunu, kaynak kodun kendisini izleyerek — işaretçinin nereye gittiğini, hangi arayüze geçtiğini, hangi kapsayıcıya eklendiğini takip ederek — karar veriyor. Bu izleme işlemi kaçış çözümlemesi: bir değerin, onu oluşturan kapsamdan “kaçıp kaçmadığını” belirleyen derleme adımı.
Yığıt ile öbek arasındaki fark yalnız “nerede durduğu” değil, “kim yönettiği”. Yığıttaki bir değerin ömrü çağrı çerçevesiyle birlikte otomatik kapanıyor — ayrı bir temizleme adımı gerekmiyor. Öbekteki bir değerin ömrü ise çağrı çerçevesinden bağımsız ve bu değerin ne zaman kaldırılacağına program değil çalışma zamanı karar veriyor. Bu ikinci soru — öbeğe taşınan bir değer bir gün nasıl kalkıyor — sıradaki dersin konusu; bu ders yalnız değerin öbeğe gidip gitmediğine bakıyor.
Önceki derste bicimle işlevinin ayrintili dalı yedi fazladan değer ayırıyordu ve bu
derste o yedi ayırmanın nereden geldiği açıklığa kavuşuyor: üç fmt.Sprintf çağrısının
her biri, ürettiği dizgiyi ve argümanlarını bir arayüze sararken tam olarak bu derste
görülecek türden bir kaçış kararı veriyordu. Profil o kararların sonucunu (kaç değer)
saydı; bu ders kararın kendisini okuyor.
Kararı Tek Başına Okumak
Kaçış çözümlemesinin kararını okumanın yolu, derleyiciyi program içinden çağırmak ve metnini değil, yalnız kararını okumak. Aşağıdaki yardımcı işlev bunu yapıyor: verilen bir kaynak parçasını kendi başına, geçici bir modüle yazıyor, derleyiciyi bu modül üzerinde koşturuyor ve çıktıda öbeğe taşıma bildirimi olup olmadığını arıyor — bildirimin metni hiçbir zaman basılmıyor, yalnız bu bir doğruluk değeri.
// kacis.go — derleyicinin obege tasima kararini program icinden okuyor, metnini basmiyor
package main
import (
"os"
"os/exec"
"strings"
)
// kaciyorMu, verilen govdeyi tek basina bir pakete yaziyor ve derleyiciyi
// bu paket uzerinde kosturuyor. Donen deger yalniz bir dogruluk degeri;
// derleyicinin bildirim metni hicbir zaman okunmuyor ya da basilmiyor.
func kaciyorMu(govde string) bool {
dizin, err := os.MkdirTemp("", "kacis")
if err != nil {
return false
}
defer os.RemoveAll(dizin)
os.WriteFile(dizin+"/go.mod", []byte("module kacis\n\ngo 1.24\n"), 0o644)
os.WriteFile(dizin+"/deger.go", []byte(govde), 0o644)
cmd := exec.Command("go", "build", "-gcflags=-m -l", "-o", os.DevNull, ".")
cmd.Dir = dizin
cmd.Env = append(os.Environ(), "GOTOOLCHAIN=local")
cikti, _ := cmd.CombinedOutput()
metin := string(cikti)
return strings.Contains(metin, "moved to heap") || strings.Contains(metin, "escapes to heap")
}
BS4. kaciyorMu hedef işlevi hiç çağırmıyor — yalnız tanımlıyor. Derleyici, bir işlevi
çağıran bir kod olmasa bile onun gövdesini derleme sırasında çözümlüyor ve kararını
bildiriyor; bu yüzden kaciyorMunun geçici modülünde func main() {} boş kalabiliyor. Bu,
kararın çağrılmaya değil tanıma bağlı olduğunu gösteriyor — kaçış çözümlemesi koşum
gerektirmiyor, çünkü koşan bir şey yok; yalnız derlenen bir şey var.
Her kaynağın kendi başına, ayrı bir pakette derlenmesi de bilinçli bir seçim. Derleyici bazı küçük işlevleri çağrıldıkları yere açabiliyor; böyle bir açma olduğunda kaçış kararı işlevin kendi gövdesine değil, açıldığı çağrı yerine yazılıyor ve okuması güçleşiyor. Her kaynağı yalnız kendi paketinde, başka hiçbir çağrıya karışmadan derlemek bu karmaşayı baştan kaldırıyor: kararın nereye yazıldığı, hangi işlevin kendisine ait olduğu belirsizliğe düşmüyor.
Bu izolasyonun bir bedeli de var: gerçek bir programda adresDonduren gibi bir işlev,
çağrıldığı yere açılmışsa, kararı burada ölçülenden farklı çıkabilir — çağıran taraf
sonucu hemen kullanıp bırakıyorsa, açma sonrası derleyici değeri yine de yığıtta tutmayı
başarabilir. Bu ders o durumu ölçmüyor; yalnız işlevin kendi başına, hiçbir çağrıya
açılmadan verdiği kararı ölçüyor. Gerçek bir programdaki karar bu yüzden burada görülenden
daha iyimser çıkabilir, hiçbir zaman daha kötümser değil — açma yalnız kaçışı önleyebilir,
hiçbir zaman yeni bir kaçış eklemiyor.
Dört İşlev, Bir Karar Tablosu
Aşağıdaki ölçüm dört ayrı işlev tanımlıyor, dördü de aynı Kayit yapısını kullanıyor ama
dördü de değeri farklı bir yolla ele alıyor: biri değeri yalnız yerel olarak okuyor, biri
adresini döndürüyor, biri bir arayüze veriyor, biri bir dilime ekliyor. Beşinci kaynak,
birincinin (yerelKalan) neredeyse birebir aynısı — tek fark, son satırın bir değer yerine
bir adres döndürmesi.
// olcum.go — dort islev, derleyicinin obege tasima karari, ve kucuk bir degisiklik
package main
import "fmt"
const yerelKalan = `package main
type Kayit struct{ Ad string }
func yerelKalan() int {
k := Kayit{Ad: "vida"}
return len(k.Ad)
}
func main() {}
`
const adresDonduren = `package main
type Kayit struct{ Ad string }
func adresDonduren() *Kayit {
k := Kayit{Ad: "somun"}
return &k
}
func main() {}
`
const arayuzeVerilen = `package main
import "fmt"
type Kayit struct{ Ad string }
func arayuzeVerilen(k Kayit) {
fmt.Println(k)
}
func main() {}
`
const dilimeEklenen = `package main
type Kayit struct{ Ad string }
func dilimeEklenen(hedef []*Kayit) []*Kayit {
k := Kayit{Ad: "civi"}
return append(hedef, &k)
}
func main() {}
`
const yerelKalanDegisik = `package main
type Kayit struct{ Ad string }
func yerelKalanDegisik() *string {
k := Kayit{Ad: "vida"}
return &k.Ad
}
func main() {}
`
func main() {
satirlar := []struct {
ad string
kaynak string
}{
{"yerelde kalan deger", yerelKalan},
{"adres donduren deger", adresDonduren},
{"arayuze verilen deger", arayuzeVerilen},
{"dilime eklenen deger", dilimeEklenen},
{"ayni islev, kucuk degisiklikle", yerelKalanDegisik},
}
kacan := 0
fmt.Println("-- derleyicinin obege tasima karari --")
for i, s := range satirlar {
tasindi := kaciyorMu(s.kaynak)
if tasindi && i < 4 {
kacan++
}
fmt.Printf("%-32s obege tasindi=%t\n", s.ad, tasindi)
}
fmt.Println("dort islevden obege tasinan:", kacan, "/ 4")
}
-- derleyicinin obege tasima karari -- yerelde kalan deger obege tasindi=false adres donduren deger obege tasindi=true arayuze verilen deger obege tasindi=true dilime eklenen deger obege tasindi=true ayni islev, kucuk degisiklikle obege tasindi=true dort islevden obege tasinan: 3 / 4
Sayıların Okunması
Dört işlevin üçü öbeğe taşınıyor, biri kalmıyor. yerelKalan değeri yalnız kendi
gövdesinde okuyor (len(k.Ad)) ve dışarı hiçbir şey vermiyor — değer, çağrı çerçevesiyle
birlikte kapanabiliyor, yığıtta kalıyor. Öbür üçü, değeri üç ayrı yoldan dışarı
taşıyor: adresDonduren değerin adresini döndürüyor, arayuzeVerilen değeri bir arayüz
parametresine (fmt.Printlnnin değişken sayıda argüman alan ...any parametresi)
geçiriyor, dilimeEklenen değerin adresini bir dilime ekliyor. Üçünün de ortak yanı: değer,
onu oluşturan çağrı çerçevesinden daha uzun yaşamak zorunda — çağrı bittikten sonra da
birileri o değere erişebilecek. Derleyici bunu görüyor ve değeri öbeğe taşıyarak çağrı
çerçevesinin ömründen bağımsızlaştırıyor.
Beşinci satır bunu daha keskin gösteriyor: yerelKalanDegisik, yerelKalanın neredeyse
aynısı — aynı yapı, aynı alan adı, aynı ilk satır. Tek fark son satırda: return len(k.Ad) yerine return &k.Ad. Bu tek değişiklik, dönen değeri bir tam sayıdan bir
adrese çeviriyor ve kararı falsetan trueya döndürüyor. Kaynakta hiçbir yeni
işaretçi türü, hiçbir açık bellek ayırma çağrısı yok — yalnız & işareti eklendi. Kaçış
çözümlemesi kararı, kodun niyetine değil, değerin erişilebilirliğine bakıyor;
bir alanın adresini almak, o alanı tutan yapının artık çağrı çerçevesinde kalamayacağı
anlamına geliyor.
Bu asimetri, yığıtta kalmanın neden varsayılan değil bir kazanım sayıldığını
açıklıyor. Bir değeri yığıtta tutmak için özel bir şey yazmaya gerek yok — yalnız değeri
dışarı taşımamak yeterli. Ama bu “taşımama” kırılgan: dilimeEklenen örneğinde olduğu gibi
bir dilime eklemek, arayuzeVerilen örneğinde olduğu gibi bir arayüze geçirmek, ya da
yerelKalanDegisik örneğinde olduğu gibi tek bir alanın adresini almak, dördü de aynı
yönde çalışıyor: değeri yığıttan öbeğe itiyor. Tersi yönde bir işlem yok — bir değeri
“öbeğe zorlamak” için tek satır yeterken, öbekten kurtarmak için kodun o değeri hiç
dışarı vermemesi gerekiyor. Bu yüzden dört işlevin üçünün kaçması bu örnek kümesine özgü bir
tesadüf değil: dışarı veren her işlem aynı yöne itiyor.
Kursun açılışı on bir bilgiyi kaynak, araç ve koşum diye ayırmıştı; bu ders o üç sınıftan yalnız aracı kullanıyor — kaçan değer sayısı ne kaynakta yazılı ne yalnız koşarak öğreniliyor, derleyici bunu koşum olmadan türetip bildiriyor.
kaciyorMunun tek bir doğruluk değeri döndürmesi, derleyicinin aslında iki ayrı türde
bildirim verdiği gerçeğini gizliyor. adresDonduren gibi bir işlevde bildirim yerel bir
değerin taşındığını söylüyor; arayuzeVerilen gibi bir işlevde ise bildirim bir
parametrenin kendisinin çağıranın kapsamına “sızdığını” söylüyor, çünkü değer zaten
çağıran tarafından üretilip parametre olarak geçiriliyor. İkisi de sonuçta aynı yere
varıyor — değer öbekte duruyor — ama yolu farklı: biri işlevin içinde oluşan bir değeri,
öbürü işleve dışarıdan gelen bir değeri taşıyor. Bu dersin ölçümü bu ikisini tek bir
true/false altında birleştiriyor çünkü ikisinin de ortak sonucu — öbeğe taşınma —
kursun sorduğu soru; yolun ayrıntısı derleyicinin iç işleyişine ait ve burada okunmuyor.
Sınırlayıcı Ölçüm: Bu Bir Söz Değil, Bir Karar
Yukarıdaki tabloyu okuyan biri hızla “Go’da bir değerin adresi alınırsa öbeğe gider” gibi bir kurala varabilir. Bu kural bugünkü derleyici için doğru çıkıyor ama dilin sözü değil. Dil belirtimi hiçbir yerde “adresi alınan değer öbeğe gider” demiyor; yalnız bir değerin ne kadar yaşayacağını garanti ediyor, nerede durduğunu değil. Kaçış çözümlemesi bu garantiyi yerine getirmenin bir yöntemi — belirli bir derleyicinin, belirli bir sürümde seçtiği bir gerçekleştirim kararı.
Bunun somut sonucu şu: daha akıllı bir derleyici, adresDonduren gibi bir işlevi de
yığıtta tutmayı başarabilir — örneğin çağıran taraf sonucu hemen kullanıp atıyorsa ve
derleyici bunu kanıtlayabiliyorsa. Bu ders o kanıtlamayı görmedi çünkü bu dersteki dört
işlevin hiçbiri böyle bir kanıtlamaya izin vermiyor — hepsi değeri gerçekten çağrı
çerçevesinin dışına taşıyor. Ama aynı kaynak, farklı bir derleyici sürümüyle derlendiğinde
farklı bir karar verebilir; bu ders bu yüzden karar tablosunu bir gerçek olarak değil,
bu derlemenin bir gözlemi olarak sunuyor.
Aynı belirsizlik dilin belirtimi ile dilin gerçekleştirimi arasındaki genel ayrımın bir örneği. Belirtim bir değerin ne kadar yaşayacağını, hangi işlemlerin geçerli olduğunu garanti ediyor; gerçekleştirim bu garantiyi hangi bellek alanında, hangi yöntemle tuttuğunu seçiyor. Aynı ayrım derleyicinin başka kararlarında da geçerli — sıralama garantisi vermeyen bir sözlüğün gezinme sırası da benzer biçimde bir gerçekleştirim seçimiydi, dilin sözü değildi. Kaçış çözümlemesi bu genel örüntünün bellek yerleşimine uygulanmış hâli: gördüğünüz karar doğru, ama o karar dilin değil derleyicinin.
Bu sınır, önceki dersin sınırıyla aynı köke iniyor: bir araç bilgisi, aracın kendisiyle sınırlı. Profil aracı yalnız koşan yolu görüyordu; kaçış çözümlemesi aracı yalnız bu derleyicinin bu kararını gösteriyor. İkisi de gerçeği değil, aracın o andaki yanıtını veriyor.
Bunun pratik bir sonucu var: bir kod tabanında “bu değer öbeğe kaçıyor, düzeltilsin” diye
yazılmış bir yorum, derleyici değiştiğinde yanlış çıkabilir. Kaçış kararı kaynakla birlikte
saklanan bir gerçek değil, derleme anında yeniden üretilen bir çıktı. Bu yüzden bu dersteki
tablo da tek seferlik değil — aynı kaciyorMu çağrısı her çalıştırıldığında derleyiciyi
yeniden koşturuyor ve kararı yeniden soruyor; hiçbir karar önceki bir koşumdan
saklanmıyor.
Özet
- Kaçış çözümlemesi bir araç bilgisidir: derleyici, programı hiç çalıştırmadan, bir değerin yığıtta mı öbekte mi ayrılacağına karar veriyor ve bunu koşum olmadan bildiriyor.
- Dört işlevin üçü öbeğe taşındı — değerin adresini döndüren, bir arayüze veren ve bir dilime ekleyen. Değeri yalnız kendi gövdesinde tutan işlev yığıtta kaldı.
- Aynı işlevin bir satırlık değişikliği (
len(k.Ad)yerine&k.Ad) kararıfalsetantrueya çevirdi — kaçış çözümlemesi kodun niyetine değil, değerin erişilebilirliğine bakıyor. - Bu karar dilin bir sözü değil, derleyicinin sürümüne bağlı bir gerçekleştirim ayrıntısı; başka bir derleyici sürümü aynı kaynak için farklı bir karar verebilir.
- Yığıt ve öbek kavramı burada tekrar kurulmadı; bu ders yalnız derleyicinin bu ikisi arasındaki kararını ölçtü.
- Kaçış kararı yalnız bir yönde çalışıyor: bir değeri dışarı vermek onu öbeğe iten her yeni yol, kaçışı artırıyor; hiçbir işlem tek başına bir değeri öbekten geri çağırmıyor.
Sonraki Adım
Bu ders bir değerin öbeğe taşınıp taşınmadığını sordu — tek seferlik, derleme anında verilen bir karar. Ama bir programın gerçek çalışma sürecinde bu karar tek başına yeterli değil: öbeğe taşınan her değer bir gün oradan da kalkması gerekiyor ve bu kalkışın kendisi derleme anında değil koşum sırasında oluyor. Sıradaki ders bu ikinci yarıya bakıyor — bu derste kurulan “öbeğe gitme” kararının toplamda ne kadar biriktiği, belirli tekniklerin bu toplamı nasıl düşürdüğü ve bir sayının düşmesinin her zaman bir iyileşme anlamına gelmediği.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.