Ders 09 / 16
Çöp Toplama Davranışı
Ayırma baskısının azalması sayıyla ölçülüyor: önceden yer ayırma, arabellek yeniden kullanımı ve değer/işaretçi seçimi aynı işin ayırma sayısını dörtten bire, dörtten sıfıra, birden altmış beşe taşıyor. Sınırlayıcı ölçüm: bir sayının düşmesi her zaman bir iyileştirme değil — okunabilirliği bozan bir yazım da aynı sayıya ulaşabiliyor.
Yayın Güncelleme
Bu derste
- Önceden yer ayırmanın, arabellek yeniden kullanımının ve değer/işaretçi seçiminin ayırma sayısına etkisini beş koşumda ölçer
- Aynı ayırma sayısına biri okunaklı biri okunaksız iki farklı yazımla ulaşılabildiğini koşturarak gösterir
- Ayırma sayısını düşürmenin, kaçış çözümlemesindeki kararı otomatik olarak iyileştirmediğini ayırt eder
İçindekiler
Önceki ders bir değerin öbeğe taşınıp taşınmadığını, derleme anında verilen tek seferlik bir karar olarak ölçtü. Ama bir program bir kez değil binlerce kez çalışıyor ve öbeğe giden her değer bir gün oradan da kalkması gerekiyor. Bu ders o kalkışın yükünü sorguluyor: aynı işi yapan farklı yazımlar, toplamda kaç değeri öbeğe gönderiyor ve bu sayı belirli tekniklerle nasıl düşüyor?
Çöp toplamanın kendisi — erişilebilirlik, kök kümesi, işaretle ve süpür adımları, duraklatmanın yaşayan nesneyle orantılı oluşu — İşletim Sistemi Kavramları kursunda (M01/K05) kavram olarak kuruldu ve burada tekrarlanmıyor. Bu ders kuramı değil sonucu ölçüyor: öbeğe daha az değer göndermek, toplayıcının daha az iş yapması anlamına geliyor. Duraklatma süresi, geri kazanılan bellek miktarı ve nesil boyutu bu derste hiç yazılmıyor — hepsi ortama bağlı; sayılan tek şey ayırma sayısı.
Bu daraltmanın gerekçesi önceki kurstan geliyor: o kurs, toplayıcının işinin yaşayan nesnelerle orantılı olduğunu kurmuştu — ne kadar çok nesne öbekte duruyorsa taraması o kadar uzun sürüyor. Bu ders o ilişkinin girdi ucuna bakıyor: bir program öbeğe daha az nesne gönderirse, tarayacak daha az şey oluyor. Aradaki bağ dolaylı — bu ders duraklatmayı hiç ölçmüyor — ama yön açık: ayırma sayısı düşerse toplayıcının işi de düşme eğiliminde. Bu dersin ölçtüğü, o düşüşün girdi tarafı; toplayıcının bunu nasıl işlediği önceki kursun konusu.
Ayırma Baskısı: Üç Teknik, Bir Ölçüm
“Ayırma baskısı” bir programın birim zamanda öbeğe ne kadar yeni değer gönderdiğini anlatıyor. Baskı yüksekse toplayıcı daha sık çalışmak zorunda kalıyor; baskı düşükse daha seyrek. Bu ders baskıyı süre üzerinden değil, sayı üzerinden ölçüyor: aynı işin bir kere çalıştırılması kaç değer ayırıyor. Üç teknik bu sayıyı düşürüyor — önceden yer ayırma, arabellek yeniden kullanımı, değer/işaretçi seçimi — ve dördüncü bir örnek bu sayının tek başına yeterli olmadığını gösteriyor.
Dördü de aynı ölçüm kalıbını paylaşıyor: bir işlev tanımlanıyor, o işlevi çağıran bir karşılaştırmalı ölçüm işlevi yazılıyor ve bu ölçüm beş kez koşturulup kaç ayrı değer geldiği sayılıyor. Önceki iki dersten farklı olarak burada tek bir değer değil, dört ayrı karşılaştırma yan yana duruyor — her biri kendi çiftiyle (ham/ayrılmış, yeni/yeniden, değer/işaretçi) bir öncesi ve sonrasını gösteriyor.
// olcum.go — uc teknigin ayirma sayisina etkisi, bes kosumda
package main
import (
"fmt"
"strconv"
"testing"
)
type Kayit struct {
Ad string
Miktar int
}
const kayitSayisi = 64
var toplamYut, uzunlukYut, kayitYut int
// --- a) onceden yer ayirma ---
//go:noinline
func topluOlustur(n int) []int {
var d []int
for i := 0; i < n; i++ {
d = append(d, i)
}
return d
}
//go:noinline
func topluOlusturAyrilmis(n int) []int {
d := make([]int, 0, n)
for i := 0; i < n; i++ {
d = append(d, i)
}
return d
}
func benchTopluHam(b *testing.B) {
for i := 0; i < b.N; i++ {
toplamYut += len(topluOlustur(kayitSayisi))
}
}
func benchTopluAyrilmis(b *testing.B) {
for i := 0; i < b.N; i++ {
toplamYut += len(topluOlusturAyrilmis(kayitSayisi))
}
}
// --- b) arabellek yeniden kullanimi ---
//go:noinline
func satirOlustur(n int) []byte {
var buf []byte
for i := 0; i < n; i++ {
buf = append(buf, strconv.Itoa(i)...)
buf = append(buf, ' ')
}
return buf
}
//go:noinline
func satirOlusturYeniden(buf []byte, n int) []byte {
buf = buf[:0]
for i := 0; i < n; i++ {
buf = append(buf, strconv.Itoa(i)...)
buf = append(buf, ' ')
}
return buf
}
var paylasilanArabellek = make([]byte, 0, 8)
func benchArabellekYeni(b *testing.B) {
for i := 0; i < b.N; i++ {
uzunlukYut += len(satirOlustur(16))
}
}
func benchArabellekYeniden(b *testing.B) {
for i := 0; i < b.N; i++ {
paylasilanArabellek = satirOlusturYeniden(paylasilanArabellek, 16)
uzunlukYut += len(paylasilanArabellek)
}
}
// --- c) deger / isaretci secimi ---
//go:noinline
func degerDilimiOlustur(n int) []Kayit {
d := make([]Kayit, 0, n)
for i := 0; i < n; i++ {
d = append(d, Kayit{Ad: "x", Miktar: i})
}
return d
}
//go:noinline
func isaretciDilimiOlustur(n int) []*Kayit {
d := make([]*Kayit, 0, n)
for i := 0; i < n; i++ {
d = append(d, &Kayit{Ad: "x", Miktar: i})
}
return d
}
func benchDegerDilimi(b *testing.B) {
for i := 0; i < b.N; i++ {
kayitYut += len(degerDilimiOlustur(kayitSayisi))
}
}
func benchIsaretciDilimi(b *testing.B) {
for i := 0; i < b.N; i++ {
kayitYut += len(isaretciDilimiOlustur(kayitSayisi))
}
}
// --- d) karisik yazim: ayni sayiya baska bir yoldan ulasiyor ---
//go:noinline
func topluOlusturKarisik(n int) []int {
kapasite := 0
for sayac := 0; sayac < n; sayac++ {
kapasite++
}
d := make([]int, 0, kapasite)
for i := 0; i < n; i++ {
d = append(d, i)
}
return d
}
func benchTopluKarisik(b *testing.B) {
for i := 0; i < b.N; i++ {
toplamYut += len(topluOlusturKarisik(kayitSayisi))
}
}
func besKosum(f func(*testing.B)) (int64, int) {
degerler := map[int64]bool{}
var ilk int64
for i := 0; i < 5; i++ {
r := testing.Benchmark(f)
degerler[r.AllocsPerOp()] = true
if i == 0 {
ilk = r.AllocsPerOp()
}
}
return ilk, len(degerler)
}
func satir(ad string, f func(*testing.B)) {
ayirma, ayriDeger := besKosum(f)
fmt.Printf("%-30s ayirma=%-3d bes kosumda ayri deger=%d\n", ad, ayirma, ayriDeger)
}
func main() {
fmt.Println("-- ayirma baskisi: teknik ayirma sayisini nasil degistiriyor --")
satir("onceden ayrilmamis dilim", benchTopluHam)
satir("onceden ayrilmis dilim", benchTopluAyrilmis)
satir("her cagrida yeni arabellek", benchArabellekYeni)
satir("yeniden kullanilan arabellek", benchArabellekYeniden)
satir("deger dilimi", benchDegerDilimi)
satir("isaretci dilimi", benchIsaretciDilimi)
satir("karisik yazim (ayni sayi)", benchTopluKarisik)
}
-- ayirma baskisi: teknik ayirma sayisini nasil degistiriyor -- onceden ayrilmamis dilim ayirma=4 bes kosumda ayri deger=1 onceden ayrilmis dilim ayirma=1 bes kosumda ayri deger=1 her cagrida yeni arabellek ayirma=4 bes kosumda ayri deger=1 yeniden kullanilan arabellek ayirma=0 bes kosumda ayri deger=1 deger dilimi ayirma=1 bes kosumda ayri deger=1 isaretci dilimi ayirma=65 bes kosumda ayri deger=1 karisik yazim (ayni sayi) ayirma=1 bes kosumda ayri deger=1
BS5. Yedi işlevin hepsi //go:noinline ile işaretlendi — dilin kendi derleme
yönergesi, derleyiciye bu işlevi çağrıldığı yere açmamasını söylüyor. Bunun nedeni önceki
dersle aynı: bir işlev çağrıldığı yere açılırsa dönen değerin çağıran tarafından hemen
kullanılıp atıldığı görülebiliyor ve bu, karşılaştırmanın ölçmek istediği ayırma davranışını
gizleyebiliyor. Yönerge, dört tekniğin ayırma sayısını kendi gövdesinde, herhangi bir
çağırana karışmadan ölçmeyi sağlıyor.
toplamYut, uzunlukYut ve kayitYut paket düzeyinde üç değişken olarak duruyor ve her
karşılaştırmalı ölçüm işlevi kendi döndürdüğü değeri bunlardan birine yazıyor. Bunun
amacı sonucu “kullanmak”: bir işlevin dönüş değeri hiçbir yere yazılmazsa derleyici o
çağrının tamamını gereksiz sayıp kaldırabiliyor ve o zaman ölçülen şey işlevin kendisi
değil boş bir döngü olurdu. Sonucu bir pakete yazmak bu riski kaldırıyor — çağrı gerçekten
oluyor ve gerçekten ayırıyor.
Sayıların Okunması
Önceden yer ayırma (a): topluOlustur, boş bir dilime tek tek ekleyerek 64 elemana ulaşıyor;
dilim her kapasitesi dolduğunda büyüyor ve her büyüme bir yeniden ayırma. Bu büyüme dört kez
tetikleniyor. topluOlusturAyrilmis aynı 64 elemanı, make([]int, 0, 64) ile baştan doğru
kapasitede açılan bir dilime ekliyor — büyüme hiç tetiklenmiyor; tek ayırma yalnız ilk make
çağrısına ait. Dört ayırmadan bire düşüş, kapasitenin önceden bilindiği durumlarda büyümenin
gereksiz olduğunu gösteriyor.
Arabellek yeniden kullanımı (b): satirOlustur her çağrıda sıfırdan başlayan bir
[]byte açıyor ve 16 parçayı buna ekleyerek dört kez büyütüyor. satirOlusturYeniden ise
paket düzeyinde saklanan tek bir arabelleği her çağrıda buf[:0] ile sıfır uzunluğa
indirgeyip yeniden dolduruyor; kapasite önceki çağrılardan zaten yeterli boyuta ulaştığı
için yeni bir ayırma hiç gerekmiyor. Dörtten sıfıra düşüş, önceki tekniktekinden daha
keskin — çünkü burada yalnız büyüme sayısı değil, her çağrıdaki ilk ayırma da ortadan
kalkıyor.
Bu sıfır sayısının bir önkoşulu var: arabelleğin kapasitesi, art arda gelen çağrıların
hiçbirinde aşılmıyor. paylasilanArabellek sekiz baytlık bir kapasiteyle açılıyor ama ilk
birkaç çağrı sırasında kendiliğinden büyüyor, çünkü satirOlusturYeniden da append
kullanıyor ve kapasite yetmediğinde büyüme kaçınılmaz. Beş koşumun beşinde de sıfır
çıkması, bu büyümenin ilk çağrıda bir kez olup sonrasında hiç tekrarlanmadığını
gösteriyor — kapasite bir kere yeterli boyuta ulaştıktan sonra aynı boyuttaki her çağrı
onu yeniden kullanabiliyor. Girdi boyutu değişken olsaydı (bazen 16, bazen 1600 parça)
büyüme tekrar tekrar tetiklenir ve sıfıra düşüş kaybolurdu.
Değer/işaretçi seçimi (c): degerDilimiOlustur, 64 Kayit değerini tek bir
bitişik dilimde tutuyor — tüm elemanlar aynı ayırmanın parçası, toplam ayırma sayısı bir.
isaretciDilimiOlustur ise her elemanı &Kayit{...} ile ayrı ayrı öbeğe gönderiyor: bir
ayırma dilimin kendisi için, altmış dört ayırma da elemanların her biri için — toplam
altmış beş. Aradaki fark iki tekniktekinden çok daha büyük, çünkü burada değişen şey
büyüme kalıbı değil, kaç ayrı nesnenin var olduğu.
Bu fark önceki dersle doğrudan bağlanıyor: &Kayit{...} yazımı, önceki derste ölçülen
adresDonduren kalıbının aynısı — bir değerin adresini almak onu öbeğe itiyor. Buradaki
fark ölçek: önceki derste tek bir değerin kaderi ölçülüyordu, burada aynı karar altmış
dört kez tekrarlanıyor ve etkisi katlanarak büyüyor. Bir yapı dilimi seçmek yerine bir
işaretçi dilimi seçmek, gerçek kod tabanlarında sık görülen bir karar — genelde büyük
yapıları kopyalamaktan kaçınmak için. Bu ölçüm o kazancın bir bedeli olduğunu gösteriyor:
kopyalamaktan kaçınmak, ayırma sayısını artırıyor. Hangi bedelin hangi kazanca değdiği bu
dersin ölçemediği bir soru — yapının boyutu, kopyalama sıklığı ve kalan referans sayısı gibi
etkenlere bağlı ve durumdan duruma değişiyor.
Üç Tekniğin Ortak Noktası
Üç tekniğin de dayandığı ortak bir düşünce var: bir kez yeterince büyük ayırmak, çok
kez küçük küçük ayırmaktan daha az iş çıkarıyor. Önceden yer ayırma bunu kapasiteyi
baştan biliyorsa uyguluyor; arabellek yeniden kullanımı bunu kapasiteyi bir kez öğrenip
tekrar tekrar kullanarak uyguluyor; değer dilimi bunu elemanları ayrı ayrı değil bitişik
tutarak uyguluyor. Üçü de aynı yöne itiyor — ayrı ayrı ayırma sayısını azaltmak — ama
üçünün önkoşulu farklı: biri kapasitenin önceden bilinmesini, biri arabelleğin
paylaşılabilir olmasını, biri elemanların bitişik durabilmesini gerektiriyor. Bu
önkoşullardan biri sağlanmıyorsa teknik uygulanamıyor — kapasite önceden bilinmiyorsa
makee ne yazılacağı belirsiz kalıyor, arabellek eşzamanlı erişiliyorsa paylaşmak yarış
koşuluna yol açıyor, elemanlar farklı yaşam sürelerinde tutuluyorsa bitişik dilim onları
gereksiz yere bir arada tutuyor.
Sınırlayıcı Ölçüm: Bir Sayının Düşmesi Otomatik Bir İyileştirme Değil
Dördüncü satır, topluOlusturKarisik, topluOlusturAyrilmis ile aynı sayıya —
bire — ulaşıyor. Ama iki işlevin gövdesi aynı değil: topluOlusturKarisik kapasiteyi
doğrudan n olarak almak yerine, n kadar dönen ayrı bir sayaç döngüsüyle yeniden
hesaplıyor ve sonucu kapasite adlı bir değişkene yazıyor. Bu döngü hiçbir yeni bilgi
üretmiyor — kapasite her zaman ne eşit çıkıyor — ama kaynağı okuyan birine “neden
ayrı bir hesaplama var, n yetmiyor mu” sorusunu sorduruyor.
BS6. Bu, kursun en önemli uyarılarından biri: iki yazım aynı ayırma sayısına ulaştı,
ama biri okunaklı biri değil. Ayırma sayısı tek başına hangi kodun daha iyi olduğunu
söylemiyor — yalnız kaç değerin öbeğe gittiğini söylüyor. topluOlusturKarisikdaki
gereksiz döngü hiçbir ayırma eklemedi, çünkü yalnız bir tam sayı üzerinde çalışıyor
(tam sayılar bu boyutta yığıtta kalıyor); ama okunabilirliği bozdu ve kodu okuyanın zaman
kaybetmesine yol açtı. Bir sayı düştüğünde ya da sabit kaldığında sorulması gereken soru
“bu değişiklik neyin karşılığında geldi” — yalnızca “sayı ne oldu” değil.
Bu sınır, kaçış çözümlemesi dersindeki sınırla da bağlanıyor: bir değerin öbeğe gitmesi
kararı derleyicinin kararıydı, kodun niyeti değildi. Burada da benzer bir ayrım var —
ayırma sayısını düşürmek için yazılan kod, sayıyı gerçekten düşürebilir ama bunun neden
düştüğünü açıklamıyor. topluOlusturKarisikı okuyan biri, sayının düşük olmasının
nedeninin makein doğru kapasiteyle çağrılması olduğunu, döngünün kendisinin hiçbir işe
yaramadığını yalnız kodu satır satır izleyerek anlayabiliyor — ayırma sayısının kendisi bu
ayrımı yapmıyor.
Bu örnek uydurma değil, gerçek kod tabanlarında görülen bir kalıbın küçültülmüş hâli: bir
geliştirici ayırma sayısını düşürmek için bir değişiklik yapıyor, sayı gerçekten düşüyor
ve değişiklik “başarılı” sayılıyor — ama sayıyı düşüren şeyin değişikliğin hangi
parçası olduğu hiç sorgulanmıyor. topluOlusturKarisik bu tuzağı somutlaştırıyor:
gereksiz döngü kalsa da, kaldırılsa da ayırma sayısı bir olarak kalıyor, çünkü sayıyı
belirleyen makein kapasite argümanı, döngü değil. Ölçüm aracı bu ayrımı yapmıyor; ayrımı
yapan, kodu okuyup hangi satırın hangi sayıya karşılık geldiğini izleyen kişi.
Özet
- Önceden yer ayırma, ayırma sayısını dörtten bire düşürdü: kapasite önceden biliniyorsa dilimin büyüme adımları gereksiz.
- Arabellek yeniden kullanımı, ayırma sayısını dörtten sıfıra düşürdü: paylaşılan bir arabelleği sıfırlayıp yeniden doldurmak, her çağrıda yeni bir ayırma gerektirmiyor.
- Değer dilimi bir ayırmada kaldı, işaretçi dilimi altmış beşe çıktı: eleman sayısı kadar ayrı nesne açmak, tek bir bitişik depoda tutmaktan çok daha pahalı.
- Aynı ayırma sayısına (bir) iki farklı yoldan ulaşıldı — biri doğrudan doğru kapasiteyle
makeçağıran okunaklı yazım, öbürü gereksiz bir hesaplama ekleyen okunaksız yazım. - Ayırma sayısının düşmesi ya da sabit kalması tek başına bir iyileştirme kanıtı değil; hangi değişikliğin sayıyı etkilediği kodun kendisinden okunmalı. Ayırma sayısı bir koşum bilgisidir — üç yazımın üçü de yalnız koşturularak ayrıldı.
- Üç tekniğin ortak dayanağı aynı: bir kez yeterince büyük ayırmak, çok kez küçük küçük ayırmaktan daha az iş çıkarıyor; her tekniğin kendi önkoşulu (bilinen kapasite, paylaşılabilir arabellek, bitişik durabilen eleman) sağlanmadan uygulanamıyor.
Sonraki Adım
Bu ders bir işlevin kaç kez çağrıldığında kaç değer ayırdığını sayıyor bilgisiyle sınırlı kaldı — bir programın yalnız bellek yönü. Ama gerçek bir programın çalışma zamanı bundan daha fazlasını içeriyor: hangi goroutine ne zaman başladı, kaç tanesi eşzamanlı çalıştı, hangi olay hangi sırayla oldu — bunların hiçbiri bir ayırma sayısıyla yakalanmıyor. Sıradaki ders bu daha geniş kayda bakıyor — yürütme izinin hangi olayları saydığı ve iki farklı kurulumun ürettiği olay sayılarının nasıl karşılaştırıldığı.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.