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
nodev24.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.