İçeriğe geç
academia.sh

Ders 05 / 15

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

Bir başarım farkının anlamlı sayılabilmesinin koşulu: ısınma penceresinin istek bloklarıyla ölçülmesi, aynı kodun iki kol olarak koşturulmasıyla düzeneğin kendi gürültüsünün sayılması, tek tura bakan kararın gerilemeyi tersine okuduğu turun gösterilmesi ve eşik ile tur sayısının birlikte taranarak yanlış geçme–yanlış kalma dengesinin kurulması.

İçindekiler

Önceki dersin payları tek bir taban koşumuna bölündü ve bir pay 1’i aştı. Nedeni bir kusur değildi: aynı düzenek iki kez koşturulunca iki ayrı sayı verir. Düzenek ilk saniyeyi de ölçüme katmıyordu — bir ısınma etkisinin varlığı kabul edilmiş, büyüklüğü hiç ölçülmemişti. Bu ders ölçümün kendisini sınar.

Soru bir sürüm kararıdır. Ödünç servisinin aday sürümü gişe ekranına üyenin gecikmiş kitap sayısını ekliyor ve bunun için istek başına bir sorgu daha çalıştırıyor. Yayım kapısı şunu sormak zorunda: aday sürümün ölçümü yüksek çıktığında fark sürümden mi, ölçümden mi?

Bir Farkın Anlamlı Sayılması

Aynı düzeneğin iki koşumunun aynı sayıyı vermemesine gürültü (noise), sayıların koşumdan koşuma yayılmasına koşumlar arası değişkenlik denir. Bir sürüm farkı ancak bu yayılımdan ayrılabildiği ölçüde okunabilir.

IO9 (varsayım) — aday sürümün ölçümü tabanın ölçümünü yüzde beşten fazla aşarsa gerileme sayılır ve yayım durur. Kaynağı bir gereksinim ya da ölçüm değil, kapı duyarlılığı için seçilmiş bir sayıdır; aşağıda taranır. Kararın sahibi yayım kapısıdır: kırmızıya döndüğünde aday sürüm çıkmaz.

Eşiğin iki hata biçimini sayabilmek için düzenek üç kolla koşar. taban ile denetim kolları aynı kodu çalıştırır, aday kolu yeni sürümü. Denetim kolunun gerçek farkı sıfırdır; kapı onu kırmızıya döndürürse bu bir yanlış kalmadır. Aday kolunun farkı gerçektir; kapı onu geçirirse bu bir yanlış geçmedir. İki özdeş kolu koşturmak, düzeneğin kendi hatasını okumanın en ucuz yoludur.

Düzenek

Ölçüm on tur yapar; her tur üç koşumdur ve her koşum kendi sunucu süreciyle başlar. Üç kolun art arda koşması, makine yükündeki bir kaymanın tek bir kola yüklenmesini engeller.

// kutuphane/sunucu.mjs — odunc ucu. Aday surum (b) yanita gecikmis kitap sayisini da ekler,
// yani istek basina bir sorgu daha calistirir. Argumanlar: <baglanti-noktasi> <surum a|b>
import { createServer } from "node:http";
import { DatabaseSync } from "node:sqlite";

const UYE = 500, BASLANGIC = 80000, SON = 40, GEC = 30;   // katalog boyu, liste boyu, gecikme gunu
const port = Number(process.argv[2]), surum = process.argv[3];

if (Number.isInteger(port) === false || ["a", "b"].includes(surum) === false) {
  console.log("kullanim: node kutuphane/sunucu.mjs <baglanti-noktasi> <surum a|b>");
} else {
  const db = new DatabaseSync(":memory:");
  db.exec("CREATE TABLE odunc(no INTEGER PRIMARY KEY, uye_no TEXT, kitap_no TEXT, gun INT)");
  db.exec("CREATE INDEX odunc_uye ON odunc(uye_no)");
  db.exec("BEGIN");
  const doldur = db.prepare("INSERT INTO odunc(uye_no, kitap_no, gun) VALUES(?,?,?)");
  for (let i = 0; i < BASLANGIC; i += 1) doldur.run(`U-${i % UYE}`, `K-${i % 9000}`, i % 400);
  db.exec("COMMIT");

  const say = db.prepare("SELECT COUNT(*) AS n FROM odunc WHERE uye_no = ?");
  const son = db.prepare("SELECT no, kitap_no, gun FROM odunc WHERE uye_no = ? LIMIT ?");
  const gec = db.prepare("SELECT COUNT(*) AS n FROM odunc WHERE uye_no = ? AND gun > ?");
  let istek = 0, sorgu = 0;

  createServer((gelen, yanit) => {
    const u = new URL(gelen.url, "http://yerel");
    yanit.writeHead(200, { "content-type": "application/json" });
    if (u.pathname === "/olcut") { yanit.end(JSON.stringify({ istek, sorgu })); return; }
    istek += 1;
    const uye = u.searchParams.get("uye") ?? "U-1";
    const n = say.get(uye).n, liste = son.all(uye, SON); sorgu += 2;
    let geciken = -1;
    if (surum === "b") { geciken = gec.get(uye, GEC).n; sorgu += 1; }   // aday surumun ek alani
    yanit.end(JSON.stringify({ uye, odunc: n, geciken, liste }));
  }).listen(port);
}

Üreteç önceki derslerden gelir, iki farkla: ısınmayı kendisi atmaz ve hatalı istekleri sayar. İkincisi zorunludur — bağlantısı reddedilen istek çok hızlı bitmiş görünür, yani sayılmazsa ölü bir sunucu iyi bir ölçüme benzer.

// kutuphane/olc.mjs — acik dongulu uretec. Gecikmeleri gonderim sirasina gore yazar, isinmayi
// disarida birakmaz. Argumanlar: <kol adi> <baglanti-noktasi> <tur> <hedef hiz> <sure s>
import { Agent, request } from "node:http";

const [kol, port, tur, hiz, sure] = process.argv.slice(2).map((x, i) => (i === 0 ? x : Number(x)));
const agent = new Agent({ keepAlive: true, maxSockets: 8192 });
let hata = 0;

function birIstek(ms, i) {
  const bas = performance.now();
  return new Promise((coz) => {
    const bit = () => { ms[i] = Math.round((performance.now() - bas) * 1000) / 1000; coz(); };
    const r = request({ port, path: `/odunc?uye=U-${i % 500}`, agent }, (y) => { y.resume(); y.on("end", bit); });
    r.on("error", () => { hata += 1; bit(); });   // hatali istek hizli gorunur, sayilir
    r.end();
  });
}

const olcut = () => new Promise((coz) => {
  request({ port, path: "/olcut", agent }, (y) => { let govde = "";
    y.on("data", (p) => { govde += p; }); y.on("end", () => coz(JSON.parse(govde))); }).end();
});

if (Number.isInteger(port) === false) {
  console.log("kullanim: node kutuphane/olc.mjs <kol> <baglanti-noktasi> <tur> <hedef hiz> <sure>");
} else {
  const ms = [], ucus = [];
  let borc = 0, sonT = 0, sira = 0;
  const bas = performance.now();
  while (performance.now() - bas < sure * 1000) {
    const t = (performance.now() - bas) / 1000;
    borc += hiz * (t - sonT); sonT = t;
    while (borc >= 1) { borc -= 1; ucus.push(birIstek(ms, sira++)); }
    await new Promise((c) => setTimeout(c, 5));
  }
  await Promise.all(ucus);
  const o = await olcut(), gecen = (performance.now() - bas) / 1000;
  console.log(JSON.stringify({ kol, tur, ulasilan: ms.length / gecen, hata, sorgu: o.sorgu / o.istek, ms }));
}
agent.destroy();

Tarama on turdan rastgele kümeler seçip kararı yeniden verir; üreteç doğrusal eşleniktir ve tohumu görünürdür.

// kutuphane/karsilastir.mjs — isinma, degiskenlik ve karar taramasi. Arguman: <jsonl>
import { readFileSync } from "node:fs";

const ESIK = 0.05, TOHUM = 20260731, DENEME = 500, ISINMA = 100, BLOK = 100;   // IO9, gorunur tohum
const s = readFileSync(process.argv[2], "utf8").trim().split("\n").map((x) => JSON.parse(x));
const v = (x, n = 2) => x.toFixed(n).replace(".", ",");
const g = (x, n) => String(x).padStart(n);
const yuz = (d, p) => { const t = [...d].sort((a, b) => a - b);
  return t[Math.min(t.length - 1, Math.floor((p / 100) * t.length))]; };
const TUR = [...new Set(s.map((r) => r.tur))];
const bul = (t, k) => s.find((r) => r.tur === t && r.kol === k);
const havuz = (k, bas, son) => s.filter((r) => r.kol === k).flatMap((r) => r.ms.slice(bas, son));
const P = (k) => yuz(havuz(k, ISINMA, 1e9), 95);

console.log(`${TUR.length} tur, kol basina tur basina ${Math.round(havuz("taban", 0, 1e9).length / TUR.length)} ` +
  `istek, ${s.reduce((a, r) => a + r.hata, 0)} hatali istek. Havuzda taban p95 ${v(P("taban"))} ms, denetim ` +
  `${v(P("denetim"))}, aday ${v(P("aday"))}; aday/taban ${v(P("aday") / P("taban"))} kat. Sorgu/istek: ` +
  `taban ${bul(TUR[0], "taban").sorgu}, aday ${bul(TUR[0], "aday").sorgu}.`);

console.log(`\nisinma: butun turlerin ayni istek bloklari havuzda, blok ${BLOK} istek`);
console.log("istek blogu  taban ortanca  taban p95(ms)  aday p95(ms)");
for (let i = 0; i < 5; i += 1) {
  const x = i * BLOK, y = i < 4 ? x + BLOK : 1e9, d = havuz("taban", x, y);
  console.log((i < 4 ? `${x}-${x + BLOK - 1}` : `${x}+`).padEnd(13) + g(v(yuz(d, 50), 3), 13) +
    g(v(yuz(d, 95)), 15) + g(v(yuz(havuz("aday", x, y), 95)), 14));
}

const p95 = (t, k) => yuz(bul(t, k).ms.slice(ISINMA), 95);
console.log("\ntur basina okuma: her turdan bir sayi, ilk 100 istek atilarak");
console.log("olcum          en kucuk  ortanca  en buyuk  yayilim kat");
for (const ad of ["taban p95(ms)", "denetim/taban", "aday/taban"]) {
  const d = TUR.map((t) => (ad.includes("/") ? p95(t, ad.split("/")[0]) / p95(t, "taban") : p95(t, "taban")))
    .sort((m, n) => m - n);
  console.log(ad.padEnd(15) + g(v(d[0]), 8) + g(v(yuz(d, 50)), 9) + g(v(d[d.length - 1]), 10) +
    g(v(d[d.length - 1] / d[0]), 13));
}

let t = TOHUM;
const rast = () => { t = (1103515245 * t + 12345) % 2147483648; return t / 2147483648; };
const karistir = (n) => { const a = [...Array(n).keys()];
  for (let i = n - 1; i > 0; i -= 1) { const j = Math.floor(rast() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; }
  return a; };
const orta = (d, ix) => yuz(ix.map((i) => d[i]), 50);
const tara = (n, esik, x) => {
  const dizi = (k) => TUR.map((tt) => yuz(bul(tt, k).ms.slice(x), 95));
  const T = dizi("taban"), D = dizi("denetim"), A = dizi("aday");
  let kalma = 0, gecme = 0;
  for (let i = 0; i < DENEME; i += 1) {
    const ix = karistir(TUR.length).slice(0, n), kapi = orta(T, ix) * (1 + esik);
    if (orta(D, ix) > kapi) kalma += 1;            // denetim taban ile ayni kod: gerileme yok
    if (orta(A, ix) <= kapi) gecme += 1;           // aday istek basina bir sorgu daha calistirir
  }
  return `${g(kalma, 3)}/${g(gecme, 3)}`;
};

const ESIKLER = [0.02, ESIK, 0.10, 0.20];
console.log(`\nkarar taramasi: ${DENEME} deneme, tohum ${TOHUM}; hucre = yanlis kalma / yanlis gecme`);
console.log("tur/yan  pencere         " + ESIKLER.map((e) => g(`esik %${Math.round(e * 100)}`, 11)).join(""));
for (const [n, x] of [[1, ISINMA], [2, ISINMA], [3, ISINMA], [5, ISINMA], [8, ISINMA], [1, 0], [8, 0]]) {
  console.log(g(n, 7) + "  " + (x === 0 ? "isinma dahil" : "ilk 100 atildi").padEnd(16) +
    ESIKLER.map((e) => g(tara(n, e, x), 11)).join(""));
}
# olc.sh — on tur; her turda uc kol art arda kosar: taban, denetim (taban ile ayni kod)
# ve aday. Her kosum kendi sunucu sureciyle baslar.
rm -f sonuc.jsonl
for T in $(seq 1 10); do
  for KOL in "taban a 8855" "denetim a 8856" "aday b 8857"; do
    set -- $KOL
    node kutuphane/sunucu.mjs $3 $2 & SUNUCU=$!
    sleep 0.8
    node kutuphane/olc.mjs $1 $3 $T 500 1.4 >> sonuc.jsonl
    kill $SUNUCU
    wait $SUNUCU 2>/dev/null
  done
done
node kutuphane/karsilastir.mjs sonuc.jsonl
10 tur, kol basina tur basina 698 istek, 0 hatali istek. Havuzda taban p95 2,05 ms, denetim 1,84, aday 2,37; aday/taban 1,15 kat. Sorgu/istek: taban 2, aday 3.

isinma: butun turlerin ayni istek bloklari havuzda, blok 100 istek
istek blogu  taban ortanca  taban p95(ms)  aday p95(ms)
0-99                 0,579           2,42          2,06
100-199              1,027           1,79          2,44
200-299              1,050           2,12          2,44
300-399              1,001           2,12          2,22
400+                 0,953           2,13          2,38

tur basina okuma: her turdan bir sayi, ilk 100 istek atilarak
olcum          en kucuk  ortanca  en buyuk  yayilim kat
taban p95(ms)      1,70     1,86      3,43         2,02
denetim/taban      0,52     1,03      1,07         2,05
aday/taban         0,66     1,24      1,56         2,37

karar taramasi: 500 deneme, tohum 20260731; hucre = yanlis kalma / yanlis gecme
tur/yan  pencere             esik %2    esik %5   esik %10   esik %20
      1  ilk 100 atildi      310/ 62    167/ 62      0/194      0/193
      2  ilk 100 atildi      264/107    196/108      0/245      0/252
      3  ilk 100 atildi      175/ 43     62/ 58      0/ 92      0/130
      5  ilk 100 atildi      132/  0     16/ 16      0/ 53      0/114
      8  ilk 100 atildi      141/  0      0/  0      0/  0      0/140
      1  isinma dahil        309/112     86/110      0/113      0/235
      8  isinma dahil        142/  0      0/  0      0/  2      0/140

Isınma Penceresi

Isınma (warm-up), bir sürecin ilk isteklerinin yerleşik durumdan başka bir maliyetle karşılanmasıdır. Birinci tablo ilk yüz isteğin ortancasını 0,579 milisaniye, dört yüzüncü istekten sonrasını 0,953 milisaniye okuyor: ilk blok ortancada 1,65 kat iyimser. Aynı bloğun p95’i ise 2,42, yerleşik pencerede 2,13 — kuyrukta ters yönde. Isınma iki şey birden yapıyor: derleme ve önbellek dolumu birkaç isteği çok yavaşlatıyor, üretecin eşzamanlılığı henüz kurulmadığı için kalan istekler boş kuyruk buluyor.

Asıl önemlisi son sütundur. İlk blokta aday/taban oranı 2,06/2,42, yani 0,85; yerleşik pencerede 2,38/2,13, yani 1,12. Isınma gerilemenin işaretini ters çeviriyor: o blokta aday sürüm tabandan hızlı görünüyor. Pencere bu yüzden ölçümden okunur — düzenek ilk yüz isteği atar, çünkü tablo ikinci bloktan itibaren yerleşikliği gösteriyor.

Gürültü ve Tek Tura Bakan Karar

İkinci tablo aynı sayıyı on kez okuyor. Tabanın p95’i 1,70 ile 3,43 milisaniye arasında 2,02 kat yayılıyor. Kodda hiçbir şey değişmedi; değişen tek şey turun koştuğu andır.

İkinci satır düzeneğin kendi hatasını veriyor. Denetim kolu tabanla aynı kodu çalıştırır; oranın her turda 1,00 olması beklenirken 0,52 ile 1,07 arasında geziniyor. Ortanca 1,03, yani sistematik bir kayma yok; ama yayılım 2,05 kat, yani iki özdeş sürüm arasında tek bir turda yüzde kırk sekizlik bir “fark” görülebiliyor.

Üçüncü satır gerçek farkı taşıyor: turların aday/taban ortancası 1,24, yayılım 0,66–1,56. Havuz oranı 1,15’tir; turların ortancası turları eşitler, havuz yavaş turlara ağırlık verir. En küçük değer 1’in altındadır: tabanın 3,43 milisaniye okuduğu turda aday sürüm, istek başına bir sorgu daha çalıştırdığı hâlde tabandan yüzde otuz dört hızlı ölçüldü. Tek tura bakan karar orada gerilemeyi kaçırmakla kalmaz, tersine okur.

Kaç Tur, Hangi Eşik

Üçüncü tablo iki ekseni birlikte tarıyor: yan başına toplanan tur sayısı ve eşik. Her hücre denetim kolunun yanlış kalmasını, aday kolunun yanlış geçmesini sayıyor.

Yüzde ikilik eşik hiçbir tur sayısında kurtarılamıyor: bir turda 310, sekiz turda hâlâ 141 yanlış kalma. Neden ortanca değil yayılımdır: denetim kolunun 1,07’ye çıkan okumaları sekiz turun ortancasını da kapının üstüne taşıyor. Bir eşik, aynı kodun kendisiyle karşılaştırıldığında ürettiği yayılımın altına inemez.

IO9’un yüzde beşi bu alt sınırın üstünde ve savunulabilir — ama tek turda değil. Bir turda 167 yanlış kalma ve 62 yanlış geçme var; üç turda 62/58, beş turda 16/16, sekiz turda 0/0. Aranan sayı budur: yüzde beşlik kapı, yan başına sekiz tur toplanmadan karar taşımaz.

Üst sınırı son sütun veriyor. Yüzde yirmide yanlış kalma sıfır, ama yanlış geçme sekiz turda bile 140: gerçek fark 1,24 kat olduğu için kapı ona fazla yaklaşıyor ve gerileme yayımlanıyor. Eşik, ayırması istenen farkın belirgin altında kalmak zorundadır.

Isınma satırları tabloyu ikinci kez veriyor. Sekiz turda ve yüzde beşte iki pencere de 0/0; tek turda ısınmayı içeri almak yanlış geçmeyi 62’den 110’a çıkarıyor. Isınma kararı ancak tur sayısı azken çeviriyor, hep aynı yönde: gerilemeyi gizler.

Bedel ve Kaçan Sınıf

Bedelin koşuma bağlı yanı ortama bağlıdır: otuz koşum bu makinede yetmiş saniyenin altında sürdü. Koşumdan bağımsız yanı sayılır — otuz sunucu süreci, süreç başına 80.000 satırlık dolum (toplam 2,4 milyon satır), 20.939 gönderilen istek, sıfır hatalı istek ve tarama tablosunda 28.000 yeniden verilmiş karar. Sürüm farkı da koşumdan bağımsız bir niceliğe bağlanır: istek başına iki sorguya karşı üç sorgu.

Bu ölçümün göremediği iki sınıf var. Birincisi, tarama rastgele gürültüyü sayar, sistematik sapmayı saymaz. Denetim kolunun ortancası 1,03 çıktığı için burada sapma görünmedi; görünseydi tur eklemek onu küçültmezdi, çünkü ortalama yalnız rastgele bileşeni giderir. Nedenini ayıracak olan da bu düzenek değil, kol sırasının turlar arasında döndürülmesidir.

İkincisi eşiğin biçimidir. Oransal bir kapı, tabanı yavaş olan bir uçta büyük mutlak gerilemeleri geçirir: yüzde beş burada 0,10 milisaniyedir, taban 200 milisaniye olsaydı 10 milisaniye olurdu. Kapı bu yüzden oranı değil, birinci dersin mutlak eşiğini de taşır.

Özet

  • Gürültü ölçülür, varsayılmaz: aynı kodun p95’i on turda 2,02 kat yayıldı, iki özdeş kolun oranı 0,52 ile 1,07 arasında gezindi.
  • Isınma penceresi istek bloklarıyla okunur: ilk yüz istek ortancada 1,65 kat iyimser, p95’te yüksek; net etkisi aday/taban oranını 1,12’den 0,85’e çevirmektir.
  • Tek tura bakan karar yanılır: on turun birinde aday/taban 0,66 ölçüldü ve bir sorgu daha çalıştıran sürüm tabandan hızlı göründü.
  • Eşiğin alt sınırı düzeneğin kendi yayılımıdır: yüzde ikilik kapı sekiz turda bile 141 yanlış kalma bıraktı; IO9’un yüzde beşi ancak sekiz turla 0/0’a indi.
  • Eşiğin üst sınırı gerçek farktır: yüzde yirmilik kapı 1,24 katlık gerilemeyi sekiz turda bile 140 kez geçirdi.

Sonraki Adım

Başarım testinin beş dersi bir eşiği kurdu, senaryoyu modelledi, ölçütü seçti, sonucu bir katmana atfetti ve karşılaştırmayı disipline aldı. Beşinin de altında aynı varsayım duruyor: istekler geçerliydi, niyet iyiydi. Yük üreteci gerçek bir okuyucunun yapabileceği şeyleri hızlı yaptı; hiçbir koşum, sistemi bilerek kötüye kullanan bir istemcinin ne bulacağını sormadı. Sonraki ders bu soruyu en ucuz sorulduğu yerde, programın kaynak metninde ele alır ve yine sınamanın kendisini ölçer: kaç kusur bulunuyor, kaç uyarı boşa çıkıyor, hangi kusur hiçbir eşikte görünmüyor.

İ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