İçeriğe geç
academia.sh

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.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat