İçeriğe geç
academia.sh

Ders 05 / 16

Karşılaştırmalı Ölçüm

On bir bilgi bir programda toplanıyor ve üç söyleyene bölünüyor — 2'si kaynakta yazılı, 5'i araçtan koşum olmadan geliyor, 4'ü yalnız koşarak öğreniliyor. Karşılaştırmalı ölçüm aracı bu dört koşum bilgisinden birini üretiyor: aynı işin iki yazımının ayırma sayısı, beş koşumda hep aynı değeri veriyor; sınırlayıcı ölçüm bu sayının da neyi saydığını söylemeden karşılaştırılamayacağını gösteriyor.

Yayın Güncelleme

Bu derste

  • On bir bilgiyi kaynak, araç ve koşum sınıflarına ayırıp 2/5/4 dağılımını koşturarak üretir
  • Aynı işin iki yazımının ayırma sayısını beş koşumda karşılaştırıp hangisinin tekrarlanabilir olduğunu okur
  • İş başına ayırma ile toplam ayırma sayısının ayrı ölçüler olduğunu, aynı karşılaştırmayı iki biçimde okuyarak sınırlar
İçindekiler

Önceki dört ders, bir programa dair tek tek sorular sordu: bir test geçti mi, bir vaka kaç alt teste döndü, bir istek hangi durum kodunu tetikledi, bir bağımlılığın gerçeği ile sahtesi aynı kararı verdi mi. Her soru kendi kaynak/araç/koşum sınıfını taşıyordu, ama hiçbiri bu sınıfların toplamda nasıl dağıldığını sormadı. Bu ders bir adım geri çekiliyor ve aynı soruyu topluca soruyor: bir program hakkında bilinebilecek on bir ayrı şeyi bir arada toplarsak, kaçı kaynakta yazılı, kaçını bir araç koşum olmadan söylüyor, kaçı yalnız koşarak ortaya çıkıyor?

Bu derste ayrıca kursun adının işaret ettiği ölçüm — karşılaştırmalı ölçüm — ilk kez ortaya çıkıyor. Bu ölçüm türü kendi başına iki sayı üretiyor: bir işin ne kadar bellek ayırdığı ve ne kadar sürdüğü. Bu kursta yalnız birincisi kullanılıyor; ikincisinin neden dışarıda bırakıldığı bu dersin ikinci yarısının konusu.

On Bir Bilgi, Üç Söyleyen

Aşağıdaki ölçüm küçük bir kaynağı — bir yapı, iki işlev, üç vakalık bir test tablosu, bir derleme etiketiyle korunan fazladan bir dosya — geçici bir modüle yazıyor, sonra bu tek kaynak üzerinde on bir ayrı soru soruyor. Her soru gozle yordamına, sorunun yanıtını kimin verdiğiyle (kaynak, araç, koşum) birlikte veriliyor; gozle bu üç sınıfı sayıyor. Bu, önceki dört derste ayrı ayrı sorulan soruların hepsini tek bir çatı altında topluyor; her soru kendi başına daha önce görülmüş bir düzenekle (kaynağı bir dizgi olarak okuyup saymak, bir aracı program içinden çağırıp yalnız sonucunu okumak) yanıtlanıyor.

// araclar.go — bu bilgiyi kim soyluyor cercevesinin ortak yordamlari ve iki karsilastirma yazimi
package main

import (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"
)

var kaynak, arac, kosum int

func gozle(bilgi, soyleyen, kanit string) {
	switch soyleyen {
	case "kaynak":
		kaynak++
	case "arac":
		arac++
	default:
		kosum++
	}
	fmt.Printf("%-34s %-7s %s\n", bilgi, soyleyen, kanit)
}

func paket(dosyalar map[string]string) string {
	dizin, _ := os.MkdirTemp("", "olcum")
	os.WriteFile(filepath.Join(dizin, "go.mod"), []byte("module olcum\n\ngo 1.24\n"), 0o644)
	for ad, icerik := range dosyalar {
		os.WriteFile(filepath.Join(dizin, ad), []byte(icerik), 0o644)
	}
	return dizin
}

func aracCalistir(dizin string, ortam []string, arg ...string) (string, bool) {
	cmd := exec.Command("go", arg...)
	cmd.Dir = dizin
	cmd.Env = append(append(os.Environ(), "GOTOOLCHAIN=local"), ortam...)
	cikti, err := cmd.CombinedOutput()
	return string(cikti), err == nil
}

func kacanSayisi(cikti string) int { return strings.Count(cikti, "escapes to heap") }

func kapsamOrani(cikti string) string {
	for _, s := range strings.Split(cikti, "\n") {
		if i := strings.Index(s, "coverage: "); i >= 0 {
			return strings.TrimSpace(s[i+len("coverage: "):])
		}
	}
	return "yok"
}

// dilimBuyut onceden yer ayirmadan sekiz kez ekliyor.
func dilimBuyut(b *testing.B) {
	for i := 0; i < b.N; i++ {
		var d []int
		for j := 0; j < 8; j++ {
			d = append(d, j)
		}
		_ = d
	}
}

// dilimOnceden ayni isi, sekiz elemanlik yeri once ayirarak yapiyor.
func dilimOnceden(b *testing.B) {
	for i := 0; i < b.N; i++ {
		d := make([]int, 0, 8)
		for j := 0; j < 8; j++ {
			d = append(d, j)
		}
		_ = d
	}
}

// besKosumda ayni karsilastirmali olcumu bes kez kosturur; ilk deger ve kac ayri
// deger gorulduguyle dondurur.
func besKosumda(k func(*testing.B)) (ilk int64, ayriDeger int) {
	gorulen := map[int64]bool{}
	for i := 0; i < 5; i++ {
		r := testing.Benchmark(k)
		gorulen[r.AllocsPerOp()] = true
		if i == 0 {
			ilk = r.AllocsPerOp()
		}
	}
	return ilk, len(gorulen)
}

TS13. Sorulan on bir sorunun ikisi kaynaktan yanıtlanıyor: test tablosundaki vaka sayısı ve modülün dil sürümü — ikisi de metnin kendisinde yazılı, hiçbir şey çalıştırmadan okunabiliyor.

// olcum.go — on bir bilgi, uc soyleyene bolunuyor
package main

import (
	"fmt"
	"os"
	"strings"
)

const kaynakMetni = `package main

import "fmt"

type Kayit struct{ Ad string }

func yerelKalan() int {
	k := Kayit{Ad: "vida"}
	return len(k.Ad)
}

func kacan() *Kayit {
	k := Kayit{Ad: "somun"}
	return &k
}

var surum = "bilinmiyor"

func main() {
	fmt.Println(yerelKalan(), kacan().Ad, surum)
}
`

const testMetni = `package main

import "testing"

var vakalar = []struct {
	ad      string
	girdi   string
	uzunluk int
}{
	{"kisa", "ab", 2},
	{"orta", "abcd", 4},
	{"bos", "", 0},
}

func TestUzunluk(t *testing.T) {
	for _, v := range vakalar {
		if len(v.girdi) != v.uzunluk {
			t.Errorf("%s: %d beklendi", v.ad, v.uzunluk)
		}
	}
	if yerelKalan() != 4 {
		t.Error("yerelKalan")
	}
	if kacan().Ad != "somun" {
		t.Error("kacan")
	}
}
`

const etiketliMetin = `//go:build ek

package main

func ek() int { return 1 }
`

func main() {
	fmt.Println("-- bu bilgiyi kim soyluyor --")

	dizin := paket(map[string]string{"ana.go": kaynakMetni, "ana_test.go": testMetni, "ek.go": etiketliMetin})
	defer os.RemoveAll(dizin)

	gozle("tablo vaka sayisi", "kaynak", fmt.Sprint(strings.Count(testMetni, "\t{\"")))
	gozle("modul dil surumu", "kaynak", "go.mod satirinda yazili")

	kacisCikti, _ := aracCalistir(dizin, nil, "build", "-gcflags=-m", "-o", os.DevNull, ".")
	gozle("obege kacan deger sayisi", "arac", fmt.Sprint(kacanSayisi(kacisCikti)))

	_, vetTemiz := aracCalistir(dizin, nil, "vet", "./...")
	gozle("statik cozumleme bulgusu", "arac", fmt.Sprint("temiz=", vetTemiz))

	_, caprazOldu := aracCalistir(dizin, []string{"GOOS=linux", "GOARCH=arm64"}, "build", "-o", os.DevNull, ".")
	gozle("baska platforma derlenebilirlik", "arac", fmt.Sprint("derlendi=", caprazOldu))

	etiketsiz, _ := aracCalistir(dizin, nil, "list", "-f", "{{len .GoFiles}}", ".")
	etiketli, _ := aracCalistir(dizin, nil, "list", "-tags", "ek", "-f", "{{len .GoFiles}}", ".")
	gozle("etiketle degisen dosya sayisi", "arac",
		fmt.Sprint("etiketsiz=", strings.TrimSpace(etiketsiz), " etiketli=", strings.TrimSpace(etiketli)))

	_, testGecti := aracCalistir(dizin, nil, "test", "./...")
	gozle("testin verdigi karar", "kosum", fmt.Sprint("gecti=", testGecti))

	kapsamCikti, _ := aracCalistir(dizin, nil, "test", "-cover", "./...")
	gozle("kapsanan satir orani", "kosum", kapsamOrani(kapsamCikti))

	bagliCikti, _ := aracCalistir(dizin, nil, "run", "-ldflags=-X main.surum=1", ".")
	alanlar := strings.Fields(strings.TrimSpace(bagliCikti))
	gozle("ikiliye gomulen surum bilgisi", "arac", alanlar[len(alanlar)-1])

	ilkAyirma, ayriDeger := besKosumda(dilimBuyut)
	gozle("ayirma sayisi", "kosum", fmt.Sprint(ilkAyirma, " bes kosumda ayri deger=", ayriDeger))

	suzgecCikti, _ := aracCalistir(dizin, nil, "test", "-run", "TestUzunluk", "-v", "./...")
	gozle("suzgecin sectigi test sayisi", "kosum", fmt.Sprint(strings.Count(suzgecCikti, "--- PASS")))

	fmt.Printf("bilgi: %d kaynak, %d arac, %d kosum\n", kaynak, arac, kosum)
}
-- bu bilgiyi kim soyluyor --
tablo vaka sayisi                  kaynak  3
modul dil surumu                   kaynak  go.mod satirinda yazili
obege kacan deger sayisi           arac    3
statik cozumleme bulgusu           arac    temiz=true
baska platforma derlenebilirlik    arac    derlendi=true
etiketle degisen dosya sayisi      arac    etiketsiz=1 etiketli=2
testin verdigi karar               kosum   gecti=true
kapsanan satir orani               kosum   80.0% of statements
ikiliye gomulen surum bilgisi      arac    1
ayirma sayisi                      kosum   4 bes kosumda ayri deger=1
suzgecin sectigi test sayisi       kosum   1
bilgi: 2 kaynak, 5 arac, 4 kosum

TS14. Beş bilgi bir araçtan geliyor: kaçan değer sayısı, statik çözümlemenin bulgusu, başka platforma derlenebilirlik, etiketle değişen dosya sayısı, ikiliye gömülen sürüm bilgisi. Bu beşinin ortak özelliği, koşum gerektirmemeleri — derleyici, inceleyici, çözümleyici ve bağlayıcı her çağrıldığında aynı kaynağa aynı yanıtı veriyor.

TS15. Geri kalan dört bilgi yalnız koşarak öğreniliyor: testin verdiği karar, kapsanan satır oranı, ayırma sayısı, süzgecin seçtiği test sayısı. On bir bilginin toplamı bu derste ilk kez tek bir koşumda birikiyor: 2 kaynak, 5 araç, 4 koşum. Bu, konunun bağlayıcı dağılımı — bu sayı bundan sonraki her derste tek bir cümleyle anılıyor, yeniden üretilmiyor.

Bu on bir satırın her biri, önceki dört dersin sorduğu sorularla aynı biçimde soruluyor — yalnız bu kez hepsi tek bir koşumda, tek bir kaynağa karşı. tablo vaka sayisi satırı Tablo Güdümlü Testler’in sorusunu, testin verdigi karar satırı Test Paketi Temelleri’nin sorusunu aynen tekrarlıyor; aradaki fark, burada bu soruların hepsinin aynı koşumda toplanıp bir dağılıma dönüştürülmesi. On bir bilginin dokuzu (araç ve koşum toplamı) koşum ya da en az bir araç çağrısı gerektiriyor; kaynağı tek başına okumak bu on birden yalnız ikisini veriyor — programın kendisi hakkında bilinebileceklerin büyük çoğunluğu, metnin dışında duruyor.

Karşılaştırmalı Ölçümün İki Sayısından Yalnız Biri Tekrarlanabilir

Yukarıdaki on bir satırın onuncusu — ayırma sayısı — bu dersin asıl konusu ve geri kalan onundan farklı bir biçimde ölçülüyor: testing.Benchmark aynı işi beş kez koşturuyor, AllocsPerOp her koşumdan bir sayı döndürüyor ve bu beş sayının kaç ayrı değere düştüğü ayrıca sayılıyor. Yukarıdaki koşumda bu sayı 1 — beş koşum, tek bir değer.

dilimBuyut yukarıdaki tam koşumda zaten bir kez kullanılmıştı — “ayirma sayisi” satırının kanıtı oydu. Aşağıdaki ölçüm onu yeniden çağırıyor, ama bu kez tek başına değil, karşılaştıracağı ikinci bir yazımla birlikte.

TS16. Aşağıdaki ölçüm aynı işin iki yazımını karşılaştırıyor: dilimBuyut sekiz elemanı önceden yer ayırmadan ekliyor, dilimOnceden aynı sekiz elemanı önceden ayrılmış bir yere ekliyor. İkisi de beşer kez koşturuluyor.

// karsilastirma.go — ayni isin iki yazimi, ayirma sayisiyla karsilastiriliyor
package main

import "fmt"

func main() {
	fmt.Println("-- karsilastirmali olcum: ayirma sayisi ile iki yazim --")

	onceliksizIlk, onceliksizAyri := besKosumda(dilimBuyut)
	fmt.Println("onceden yer ayirmadan, is basina ayirma:", onceliksizIlk,
		"bes kosumda ayri deger:", onceliksizAyri)

	oncedenIlk, oncedenAyri := besKosumda(dilimOnceden)
	fmt.Println("onceden yer ayirarak, is basina ayirma:", oncedenIlk,
		"bes kosumda ayri deger:", oncedenAyri)
}
-- karsilastirmali olcum: ayirma sayisi ile iki yazim --
onceden yer ayirmadan, is basina ayirma: 4 bes kosumda ayri deger: 1
onceden yer ayirarak, is basina ayirma: 0 bes kosumda ayri deger: 1

Ölçüm aracının kendisi burada, tıpkı önceki koşumda olduğu gibi, iki ayrı sayı döndürüyor — iş başına ayırma sayısı ve geçen süre. Bu ders süreyi hiçbir yerde basmıyor, ölçmüyor, karşılaştırmıyor: testing.Benchmark’ın döndürdüğü değerden yalnız AllocsPerOp okunuyor; süreyi taşıyan alan hiç kullanılmıyor. Bunun nedeni bir tercih değil, bir gözlem: süre aynı makinede, aynı kaynakla, art arda beş kez ölçülse bile koşumdan koşuma değişiyor — işletim sisteminin o an başka neyi çalıştırdığına, önbelleğin o anki durumuna, hatta ölçümün kendisinin makineyi ne kadar ısıttığına bağlı. Ayırma sayısı böyle değil: dilimBuyut her koşumda tam olarak dört kez bellek ayırıyor, çünkü dilimin alt dizisi sekiz elemana ulaşana kadar aynı büyüme kararlarından geçiyor ve bu karar zinciri koşumdan koşuma değişmiyor.

dilimOnceden ise sıfır kez ayırıyor — sekiz elemanlık yer make([]int, 0, 8) ile bir kerede ayrılıyor ve derleyicinin kaçış çözümlemesi (bu ayrımın kendisi konunun sonraki konusunda ölçülüyor) bu diziyi hiç öbeğe taşımadan yığıtta tutabiliyor. Dört ile sıfır arasındaki fark, önceden yer ayırmanın somut karşılığı: aynı sekiz eleman, aynı sırayla ekleniyor, ama biri dilimin kendi büyüme mekanizmasına (kapasitesi doldukça iki katına çıkarma) bırakılırken öbürü baştan yeteri kadar yer istiyor.

Sınırlayıcı Ölçüm: Ayırma Sayısı da Neyi Saydığını Söylemeden Karşılaştırılamaz

TS17. İki yazımın ayırma sayısı — 4 ve 0 — kendi başına “biri daha iyi” demiyor; hangi sayının neyi saydığı söylenmeden bu iki değer karşılaştırılamıyor. AllocsPerOp’un döndürdüğü sayı iş başına ayırma: testing.Benchmark, b.N’i kendi belirliyor ve iç döngüyü o kadar kez koşturup toplam ayırmayı b.N’e bölerek bu sayıyı üretiyor. Toplam ayırma sayısı — bütün koşum boyunca kaç kez bellek ayrıldığı — ayrı bir sayı ve b.N büyüdükçe büyüyor; iş başına sayı sabit kalırken toplam sayı hiç sabit kalmıyor.

Bu ayrım gözden kaçtığında yanıltıcı bir karşılaştırma doğuyor: “birinci yazım ikinci yazımdan dört kat fazla bellek ayırıyor” cümlesi doğru, ama “birinci yazım toplamda dört kat fazla bellek ayırdı” cümlesi başka bir iddia — ikinci cümle, iki koşumun b.N değerinin aynı olduğunu varsayıyor ve bu varsayım hiçbir yerde denetlenmedi. Bu dersin ölçtüğü sayı hep iş başına ayırma; toplam ayırma bu derste hiç hesaplanmadı, çünkü konunun sorusu “hangi yazım her bir işlemde daha az bellek ayırıyor” — “bu koşum toplamda ne kadar bellek ayırdı” değil. Bir karşılaştırmalı ölçüm sonucu paylaşılırken bu iki sayının hangisinin kastedildiği söylenmezse, aynı iki sayı (4 ve 0) tamamen farklı bir iddiaya kanıt gösterilebilir.

besKosumda yordamının kendisi de bu ayrımı örtük olarak taşıyor: döndürdüğü ilk değeri her koşumun AllocsPerOp’u — yani zaten iş başına bir sayı, testing.Benchmark tarafından b.N’e bölünmüş hâliyle. Bu yordam toplam ayırma sayısını hiç görmüyor, çünkü testing.Benchmark’ın kendisi bu sayıyı dışarı vermiyor; yalnız iş başına ayırma ve iş başına geçen süre döndürüyor. Bu, ölçüm aracının kendi tasarımının bir sonucu — karşılaştırmalı ölçüm araçları genellikle iş başına sayıyı tercih ediyor, çünkü farklı b.N değerleriyle koşan iki ölçümü karşılaştırılabilir kılan tek şey bu normalize edilmiş biçim.

Özet

  • On bir bilgi bir koşumda toplandı ve üç sınıfa ayrıldı: 2 kaynak, 5 araç, 4 koşum — bu dağılım konunun bağlayıcı sayısı, bundan sonraki derslerde yeniden üretilmiyor.
  • Beş araç bilgisi koşum gerektirmiyor, aynı kaynağa her çağrıda aynı yanıtı veriyor; dört koşum bilgisi yalnız çalıştırılarak öğreniliyor.
  • Karşılaştırmalı ölçüm aracı iki sayı döndürüyor, ama bu ders yalnız birini — iş başına ayırma sayısını — okudu; süre hiçbir yerde basılmadı, hiçbir yerde karşılaştırılmadı.
  • Ayırma sayısı beş koşumda hep aynı değere düştü (1 ayrı değer); bu, sayının tekrarlanabilir olduğunun kanıtı.
  • Sınırlayıcı ölçüm: iş başına ayırma ile toplam ayırma ayrı sayılar; hangisinin kastedildiği söylenmeden bir karşılaştırma okunamaz.

Sonraki Adım

Bu derste bir programa dair on bir bilginin üç sınıfa nasıl ayrıldığı ve karşılaştırmalı ölçümün iki sayısından yalnız birinin tekrarlanabilir olduğu ölçüldü. Beş araç bilgisi ve dört koşum bilgisi bu dersin verdiği örnekte kaldı; her ikisi de bundan sonraki derslerde kendi köşesinde yeniden karşımıza çıkıyor, ama on bir satırlık tam tablo bir daha basılmıyor. Bu on bir bilginin biri — kapsanan satır oranı — bu derste yalnız tek bir sayı olarak geçti: 80.0% of statements. Sıradaki ders bu sayının tam olarak neyi saydığını, hangi satırların kapsama dışında kaldığını ve üç ayrı test kümesinin bu oranı nasıl değiştirdiğini ölçüyor — konunun kapanış dersi.

İ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