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.