İçeriğe geç
academia.sh

Ders 12 / 30

Sinek Siklet

Paylaşımla bellek tasarrufu: toplu ücretlendirmede her gönderinin kendi tarife kopyasını taşıdığı düzen ile değişmeyen tarife bilgisini paylaşılan nesnede toplayan düzenin nesne sayısı ve işaretlenmiş yığın ölçümüyle karşılaştırılması, paylaşımın değiştirilemezlik zorunluluğu ve paylaşım genişliği cinsinden bedeli.

İçindekiler

Önceki dört kalıpta ölçü dosya, tip ve bağ sayısıydı; hepsi kaynak metnin büyüklüğüne bakıyordu. Bu dersin ölçüsü çalışma zamanındadır: bellek.

Toplu ücretlendirme işi iki yüz bin gönderi satırını işliyor. Her gönderi bir tarife bilgisine ihtiyaç duyar: ağırlık kademeleri tablosu, bölge katsayıları, para birimi. Bu bilginin içeriği gönderiye göre değişmez — aynı tarife kodunu taşıyan bütün gönderilerde birebir aynıdır. Gönderiye göre değişen şey ağırlık ve adrestir. Sinek siklet (flyweight) kalıbı bu ikisini ayırır: değişmeyen kısım iç durum olarak tek bir nesnede paylaşılır, değişen kısım dış durum olarak kaydın kendisinde kalır. Ölçüler kurulan nesne sayısı ve yığın kullanımıdır.

Problem: Aynı Tablonun İki Yüz Bin Kopyası

Ham satırlar ve tarife tabloları iki düzende de ortaktır. Satırlar dizinden türetildiği için her koşumda aynıdır; rastgelelik yoktur.

// veri.mjs — ham gonderi satirlari ve tarife tablolari; degerler dizinden turetilir
export const KADEME_TABLO = {
  standart: [[1, 4990], [5, 8490], [15, 14990], [30, 24990]],
  ekonomik: [[1, 3990], [5, 6990], [15, 11990], [30, 19990]],
  hizli: [[1, 6490], [5, 10990], [15, 18990], [30, 31990]],
};
export const KATSAYI_TABLO = { 34: 100, "06": 118, 35: 120, 65: 145 };
export const TARIFE_KODU = ["standart", "ekonomik", "hizli"];
const POSTA = ["34100", "06500", "35400", "65100"];

export function* hamSatirlar(n) {
  for (let i = 0; i < n; i += 1) {
    yield {
      kod: `G-${i}`,
      agirlik: 0.5 + (i % 59) / 2,
      postaKodu: POSTA[i % POSTA.length],
      tarifeKodu: TARIFE_KODU[i % TARIFE_KODU.length],
    };
  }
}

İlk düzende her kayıt kendi tarife nesnesini kurar. Bu, tabloları paylaşmamanın en doğal yoludur: kayıt kendi kendine yeter, hiçbir şeyi ortak kullanmaz.

// kopyali/kur.mjs — her gonderi kendi tarife kopyasini tasir
import { KADEME_TABLO, KATSAYI_TABLO } from "../veri.mjs";

let uretilen = 0;
export const uretilenTarife = () => uretilen;

export function kur(satirlar) {
  const liste = [];
  for (const s of satirlar) {
    uretilen += 1;
    liste.push({
      kod: s.kod,
      agirlik: s.agirlik,
      postaKodu: s.postaKodu,
      tarife: {
        kod: s.tarifeKodu,
        kademeler: KADEME_TABLO[s.tarifeKodu].map(([u, f]) => [u, f]),
        katsayilar: { ...KATSAYI_TABLO },
        paraBirimi: "kurus",
      },
    });
  }
  return liste;
}

export function ucret(g) {
  const k = g.tarife.kademeler.find(([ust]) => g.agirlik <= ust) ?? [0, 24990];
  return Math.round((k[1] * (g.tarife.katsayilar[g.postaKodu.slice(0, 2)] ?? 165)) / 100);
}

Çözüm: İç Durumu Paylaşan Bir Havuz

Sinek siklet fabrikası aynı iç durum için tek nesne döndürür. Nesne değiştirilemez olmak zorundadır; gerekçesi aşağıda ölçülüyor.

// paylasimli/tarife.mjs — sinek siklet fabrikasi: ayni tarife kodu icin tek nesne
import { KADEME_TABLO, KATSAYI_TABLO } from "../veri.mjs";

const havuz = new Map();
let uretilen = 0;

export const uretilenTarife = () => uretilen;
export const havuzBoyu = () => havuz.size;

export function tarifeAl(kod) {
  let t = havuz.get(kod);
  if (t === undefined) {
    uretilen += 1;
    t = Object.freeze({
      kod,
      kademeler: Object.freeze(KADEME_TABLO[kod].map(([u, f]) => Object.freeze([u, f]))),
      katsayilar: Object.freeze({ ...KATSAYI_TABLO }),
      paraBirimi: "kurus",
    });
    havuz.set(kod, t);
  }
  return t;
}
// paylasimli/kur.mjs — gonderi tarifeye basvuru tutar; ic durum paylasilir, dis durum satirda kalir
import { tarifeAl, uretilenTarife, havuzBoyu } from "./tarife.mjs";

export { uretilenTarife, havuzBoyu };

export function kur(satirlar) {
  const liste = [];
  for (const s of satirlar) {
    liste.push({ kod: s.kod, agirlik: s.agirlik, postaKodu: s.postaKodu, tarife: tarifeAl(s.tarifeKodu) });
  }
  return liste;
}

export function ucret(g) {
  const k = g.tarife.kademeler.find(([ust]) => g.agirlik <= ust) ?? [0, 24990];
  return Math.round((k[1] * (g.tarife.katsayilar[g.postaKodu.slice(0, 2)] ?? 165)) / 100);
}

Ücret hesabının gövdesi iki düzende aynıdır; değişen şey g.tarifenin kaça ait olduğudur.

// kosum.mjs — iki duzen ayni toplam ucreti mi veriyor, kac tarife nesnesi kuruluyor
import { hamSatirlar } from "./veri.mjs";
import * as kopyali from "./kopyali/kur.mjs";
import * as paylasimli from "./paylasimli/kur.mjs";

const N = 20000;
const topla = (duzen) => duzen.kur(hamSatirlar(N)).reduce((t, g) => t + duzen.ucret(g), 0);

const a = topla(kopyali), b = topla(paylasimli);
console.log(`kopyali   : toplam=${a}  tarife nesnesi=${kopyali.uretilenTarife()}`);
console.log(`paylasimli: toplam=${b}  tarife nesnesi=${paylasimli.uretilenTarife()}`);
console.log(`toplam farki = ${a - b}  gonderi = ${N}`);
kopyali   : toplam=463093345  tarife nesnesi=20000
paylasimli: toplam=463093345  tarife nesnesi=3
toplam farki = 0  gonderi = 20000

Yirmi bin gönderi için kopyalı düzen 20.000 tarife nesnesi kurdu, paylaşımlı düzen 3. Toplam ücret kuruşu kuruşuna aynı. Nesne sayısı ortamdan bağımsız, kesin bir sayıdır: iç durum çeşidi kadar nesne kurulur.

Yığın Ölçümü

İkinci ölçü yığın kullanımıdır ve ortama bağlıdır: aşağıdaki megabayt değerleri bu makinede node v24.18.0 ile alınmıştır; sürüm, işletim sistemi ve çöp toplayıcı ayarları değiştiğinde sayılar değişir. Değişmeyen şey büyüklük mertebesidir. Ölçüm için çöp toplayıcının elle çağrılabilmesi gerekir; bu yüzden betik --expose-gc seçeneğiyle çalıştırılır.

// bellek.mjs — bir duzeni kurup yigin kullanimini olcer; node --expose-gc ile calistirilir
import { hamSatirlar } from "./veri.mjs";

if (globalThis.gc === undefined) {
  console.error("bu betik 'node --expose-gc bellek.mjs <duzen> <adet>' ile calistirilir");
  process.exit(1);
}

const duzen = process.argv[2];
const adet = Number(process.argv[3]);
const { kur, ucret, uretilenTarife } = await import(`./${duzen}/kur.mjs`);

globalThis.gc();
const once = process.memoryUsage().heapUsed;
const liste = kur(hamSatirlar(adet));
globalThis.gc();
const sonra = process.memoryUsage().heapUsed;
const toplam = liste.reduce((t, g) => t + ucret(g), 0);
console.log(`${duzen.padEnd(11)} gonderi=${liste.length}  tarife nesnesi=${uretilenTarife()}  ` +
  `yigin=${Math.round((sonra - once) / 1048576)} MB  toplam=${toplam}`);
node --expose-gc bellek.mjs kopyali 200000
node --expose-gc bellek.mjs paylasimli 200000
kopyali     gonderi=200000  tarife nesnesi=200000  yigin=208 MB  toplam=4631039370
paylasimli  gonderi=200000  tarife nesnesi=3  yigin=20 MB  toplam=4631039370

İki yüz bin gönderi için 208 megabayta karşı 20 megabayt; oran on katın biraz üstünde. Kalan 20 megabayt dış durumun kendisidir — gönderi kodu, ağırlık, posta kodu — ve paylaşımla küçültülemez, çünkü her kayıtta farklıdır. Ölçüm iki ayrı süreçte yapıldı; aynı süreçte iki düzeni birden kurmak, birinci düzenin bıraktığı nesnelerin ikinci ölçüme karışması riskini taşır.

Toplam ücret her iki koşumda da aynı çıktı; paylaşım sonucu değiştirmedi.

Bedel: Değiştirilemezlik ve Paylaşımın Genişliği

Paylaşılan nesnenin en görünür bedeli, artık kimseye ait olmamasıdır.

// bedel.mjs — paylasimin bedeli: degistirilebilirlik, paylasim genisligi, nesne kimligi
import { hamSatirlar } from "./veri.mjs";
import * as kopyali from "./kopyali/kur.mjs";
import * as paylasimli from "./paylasimli/kur.mjs";

const N = 20000;
const k = kopyali.kur(hamSatirlar(N));
const p = paylasimli.kur(hamSatirlar(N));

const oncekiK = k.map(kopyali.ucret);
k[0].tarife.katsayilar["34"] = 300;
console.log(`kopyali   : bir kaydin tablosu degisti -> degisen sonuc = ` +
  `${k.map(kopyali.ucret).filter((v, i) => v !== oncekiK[i]).length} / ${N}`);

try {
  p[0].tarife.katsayilar["34"] = 300;
  console.log("paylasimli: ayni degisiklik kabul edildi");
} catch (e) {
  console.log(`paylasimli: ayni degisiklik reddedildi (${e.constructor.name})`);
}

console.log(`paylasimli: ayni nesneyi paylasan gonderi = ${p.filter((g) => g.tarife === p[0].tarife).length} / ${N}`);
console.log(`kopyali   : ayni nesneyi paylasan gonderi = ${k.filter((g) => g.tarife === k[0].tarife).length} / ${N}`);
const cesit = paylasimli.havuzBoyu();
console.log(`ic durum cesidi = ${cesit}  paylasim orani = ${Math.round(N / cesit)} gonderi/nesne`);
kopyali   : bir kaydin tablosu degisti -> degisen sonuc = 1 / 20000
paylasimli: ayni degisiklik reddedildi (TypeError)
paylasimli: ayni nesneyi paylasan gonderi = 6667 / 20000
kopyali   : ayni nesneyi paylasan gonderi = 1 / 20000
ic durum cesidi = 3  paylasim orani = 6667 gonderi/nesne

Kopyalı düzende bir kaydın tablosunu değiştirmek yalnız o kaydı bozdu: 1/20000. Paylaşımlı düzende aynı değişiklik Object.freeze sayesinde reddedildi ve TypeError verdi. Dondurma olmasaydı değişiklik kabul edilecek ve aynı nesneyi paylaşan 6667 gönderinin ücreti sessizce sapacaktı. Paylaşım oranı hem kazancın hem riskin ölçüsüdür: her nesne 6667 kayda hizmet ediyor, dolayısıyla o nesnedeki bir hata 6667 kayda yayılır.

İkinci bedel nesne kimliğinin anlam değiştirmesidir. Kopyalı düzende g1.tarife === g2.tarife karşılaştırması yalnız aynı kayıt için doğrudur; paylaşımlı düzende aynı tarife kodunu taşıyan her gönderi için doğrudur. Kimlik eşitliğine dayanarak “bu iki gönderi aynı tarife nesnesini kullanıyorsa aynı müşteriye aittir” gibi bir çıkarım yapan kod, paylaşıma geçildiğinde yanılır.

Üçüncü bedel havuzun kendisidir: bir dosya, bir eşleme yapısı ve her kayıt kurulurken bir arama. Havuz ayrıca hiçbir zaman boşalmaz; iç durum çeşidi sınırsızsa havuz sınırsız büyür.

Buradan uygulanmama koşulu doğrudan çıkar. Kazanç, kayıt sayısının iç durum çeşidine oranıyla büyür: bu koşumda 200.000/3. Oran 1’e yaklaşırsa — her kaydın iç durumu kendine özgüyse — paylaşılacak hiçbir şey yoktur, havuz kayıt sayısı kadar nesne tutar ve üstüne eşleme yükü eklenir. Kayıt sayısı küçükse (yüzlerce) 200 megabaytlık fark yüz kilobayta iner ve değiştirilemezlik kısıtı karşılıksız kalır.

Özet

  • Sinek sikletin çözdüğü problem, kayıt sayısı kadar kopyalanan ama kayda göre değişmeyen bilginin belleği doldurmasıdır; çözüm iç durumu paylaşılan nesnede toplamak, dış durumu kayıtta bırakmaktır.
  • Yirmi bin gönderi için kurulan tarife nesnesi 20.000’den 3’e indi ve toplam ücret kuruşu kuruşuna aynı kaldı; nesne sayısı ortamdan bağımsız bir ölçüdür.
  • İki yüz bin gönderide yığın kullanımı 208 megabayttan 20 megabayta indi; bu değerler bu makinede node v24.18.0 ile ölçüldü ve ortam değiştiğinde değişir, oran ise mertebe olarak korunur.
  • Paylaşılan nesne değiştirilemez olmak zorundadır: kopyalı düzende bir tablo değişikliği 1 kaydı bozdu, paylaşımlı düzende aynı değişiklik dondurma sayesinde reddedildi; kabul edilseydi aynı nesneyi paylaşan 6667 kaydı bozacaktı.
  • Kazanç kayıt sayısının iç durum çeşidine oranıyla büyür; oran 1’e yakınsa ya da kayıt sayısı küçükse kalıp yalnız havuz yükü ve değiştirilemezlik kısıtı ekler.

Sonraki Adım

Sinek siklette bir nesneye çok sayıda kayıt aynı başvuruyla ulaştı; nesne paylaşıldı ama erişim serbestti — isteyen tarife alanını okuyup doğrudan kullanabiliyordu. Bir sonraki problem erişimin kendisini konu edinir. Tarife tablosu uzak bir kaynaktan geliyorsa ilk erişime kadar hiç yüklenmemesi, aynı tablonun ikinci kez istenmesinde tekrar getirilmemesi ve hangi kaydın hangi tabloyu istediğinin sayılabilmesi gerekir. Üçü de asıl nesnenin gövdesine dokunmadan, aynı arayüzü sunan bir nesnenin araya girmesiyle çözülür. Sonraki ders kaynağa giden çağrı sayısını iki düzende sayar, vekil kalıbını yazar ve bedelini bir dolaylılık basamağı ile bayat veri riski cinsinden ölçer.

İ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