Atraskite JavaScript memoizacijos technikas, spartinimo strategijas ir praktinius pavyzdžius kodo našumui optimizuoti. Išmokite įdiegti memoizacijos modelius.
JavaScript Memoizacijos Modeliai: Spartinimo Strategijos ir Našumo Augimas
Programinės įrangos kūrimo srityje našumas yra svarbiausias dalykas. JavaScript, būdama universali kalba, naudojama įvairiose aplinkose, nuo front-end interneto kūrimo iki serverio pusės programų su Node.js, dažnai reikalauja optimizavimo, siekiant užtikrinti sklandų ir efektyvų vykdymą. Viena galinga technika, galinti žymiai pagerinti našumą tam tikrais atvejais, yra memoizacija.
Memoizacija yra optimizavimo technika, visų pirma naudojama pagreitinti kompiuterines programas, išsaugant brangių funkcijų iškvietimų rezultatus ir grąžinant išsaugotą rezultatą, kai vėl pasitaiko tie patys įvesties duomenys. Iš esmės, tai yra spartinimo (angl. caching) forma, skirta būtent funkcijoms. Šis metodas ypač efektyvus funkcijoms, kurios yra:
- Grynosios (Pure): Funkcijos, kurių grąžinama vertė priklauso tik nuo jų įvesties verčių, be šalutinių poveikių.
- Deterministinės: Su ta pačia įvestimi funkcija visada grąžina tą patį rezultatą.
- Brangios (Expensive): Funkcijos, kurių skaičiavimai yra kompiuteriškai intensyvūs ar reikalaujantys daug laiko (pvz., rekursinės funkcijos, sudėtingi skaičiavimai).
Šiame straipsnyje nagrinėjama memoizacijos koncepcija JavaScript kalboje, gilinamasi į įvairius modelius, spartinimo strategijas ir našumo augimą, pasiekiamą ją įgyvendinant. Išnagrinėsime praktinius pavyzdžius, kad iliustruotume, kaip efektyviai taikyti memoizaciją skirtingose situacijose.
Memoizacijos Supratimas: Pagrindinė Koncepcija
Savo esme memoizacija remiasi spartinimo principu. Kai memoizuota funkcija iškviečiama su tam tikru argumentų rinkiniu, ji pirmiausia patikrina, ar tų argumentų rezultatas jau buvo apskaičiuotas ir išsaugotas podėlyje (dažniausiai JavaScript objekte arba „Map“). Jei rezultatas randamas podėlyje, jis nedelsiant grąžinamas. Priešingu atveju funkcija atlieka skaičiavimą, išsaugo rezultatą podėlyje ir tada jį grąžina.
Pagrindinė nauda – išvengiama nereikalingų skaičiavimų. Jei funkcija iškviečiama kelis kartus su tais pačiais įvesties duomenimis, memoizuota versija skaičiavimą atlieka tik vieną kartą. Vėlesni iškvietimai gauna rezultatą tiesiai iš podėlio, o tai lemia didelį našumo pagerėjimą, ypač atliekant kompiuteriškai brangias operacijas.
Memoizacijos Modeliai JavaScript Kalboje
JavaScript kalboje galima naudoti kelis modelius memoizacijai įgyvendinti. Išnagrinėkime keletą labiausiai paplitusių ir efektyviausių:
1. Bazinė Memoizacija su Uždarąja Sritimi (Closure)
Tai yra pats fundamentaliausias memoizacijos metodas. Jis naudoja uždarąją sritį (closure), kad išlaikytų podėlį funkcijos apimtyje. Podėlis paprastai yra paprastas JavaScript objektas, kuriame raktai atspindi funkcijos argumentus, o reikšmės – atitinkamus rezultatus.
function memoize(func) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args); // Sukuriamas unikalus argumentų raktas
if (cache[key]) {
return cache[key]; // Grąžinamas rezultatas iš podėlio
} else {
const result = func.apply(this, args); // Apskaičiuojamas rezultatas
cache[key] = result; // Išsaugomas rezultatas podėlyje
return result; // Grąžinamas rezultatas
}
};
}
// Pavyzdys: Faktorialo funkcijos memoizacija
function factorial(n) {
if (n <= 1) {
return 1;
}
return n * factorial(n - 1);
}
const memoizedFactorial = memoize(factorial);
console.time('Pirmas iškvietimas');
console.log(memoizedFactorial(5)); // Apskaičiuoja ir išsaugo podėlyje
console.timeEnd('Pirmas iškvietimas');
console.time('Antras iškvietimas');
console.log(memoizedFactorial(5)); // Gaunama iš podėlio
console.timeEnd('Antras iškvietimas');
Paaiškinimas:
- Funkcija `memoize` priima funkciją `func` kaip įvesties duomenį.
- Ji sukuria `cache` objektą savo apimtyje (naudojant uždarąją sritį).
- Ji grąžina naują funkciją, kuri apgaubia pradinę funkciją.
- Ši apgaubianti funkcija sukuria unikalų raktą pagal funkcijos argumentus, naudodama `JSON.stringify(args)`.
- Ji patikrina, ar `key` egzistuoja `cache`. Jei taip, grąžina išsaugotą reikšmę.
- Jei `key` neegzistuoja, ji iškviečia pradinę funkciją, išsaugo rezultatą `cache` ir grąžina rezultatą.
Trūkumai:
- `JSON.stringify` gali būti lėtas sudėtingiems objektams.
- Rakto kūrimas gali būti problemiškas su funkcijomis, kurios priima argumentus skirtinga tvarka, arba su objektais, turinčiais tuos pačius raktus, bet skirtinga tvarka.
- Neteisingai apdoroja `NaN`, nes `JSON.stringify(NaN)` grąžina `null`.
2. Memoizacija su Individualiu Rakto Generatoriumi
Siekiant išspręsti `JSON.stringify` trūkumus, galima sukurti individualią rakto generatoriaus funkciją, kuri sugeneruoja unikalų raktą pagal funkcijos argumentus. Tai suteikia daugiau kontrolės, kaip podėlis yra indeksuojamas, ir tam tikrais atvejais gali pagerinti našumą.
function memoizeWithKey(func, keyGenerator) {
const cache = {};
return function(...args) {
const key = keyGenerator(...args);
if (cache[key]) {
return cache[key];
} else {
const result = func.apply(this, args);
cache[key] = result;
return result;
}
};
}
// Pavyzdys: Dviejų skaičių sudėties funkcijos memoizacija
function add(a, b) {
console.log('Skaičiuojama...');
return a + b;
}
// Individualus rakto generatorius sudėties funkcijai
function addKeyGenerator(a, b) {
return `${a}-${b}`;
}
const memoizedAdd = memoizeWithKey(add, addKeyGenerator);
console.log(memoizedAdd(2, 3)); // Apskaičiuoja ir išsaugo podėlyje
console.log(memoizedAdd(2, 3)); // Gaunama iš podėlio
console.log(memoizedAdd(3, 2)); // Apskaičiuoja ir išsaugo podėlyje (kitas raktas)
Paaiškinimas:
- Šis modelis yra panašus į bazinę memoizaciją, bet jis priima papildomą argumentą: `keyGenerator`.
- `keyGenerator` yra funkcija, kuri priima tuos pačius argumentus kaip ir pradinė funkcija ir grąžina unikalų raktą.
- Tai leidžia lanksčiau ir efektyviau kurti raktus, ypač funkcijoms, kurios dirba su sudėtingomis duomenų struktūromis.
3. Memoizacija su „Map“
`Map` objektas JavaScript kalboje suteikia patikimesnį ir universalesnį būdą saugoti podėlio rezultatus. Skirtingai nuo paprastų JavaScript objektų, `Map` leidžia naudoti bet kokio tipo duomenis kaip raktus, įskaitant objektus ir funkcijas. Tai pašalina poreikį argumentus paversti eilutėmis ir supaprastina raktų kūrimą.
function memoizeWithMap(func) {
const cache = new Map();
return function(...args) {
const key = args.join('|'); // Sukuriamas paprastas raktas (gali būti sudėtingesnis)
if (cache.has(key)) {
return cache.get(key);
} else {
const result = func.apply(this, args);
cache.set(key, result);
return result;
}
};
}
// Pavyzdys: Eilučių sujungimo funkcijos memoizacija
function concatenate(str1, str2) {
console.log('Sujungiama...');
return str1 + str2;
}
const memoizedConcatenate = memoizeWithMap(concatenate);
console.log(memoizedConcatenate('hello', 'world')); // Apskaičiuoja ir išsaugo podėlyje
console.log(memoizedConcatenate('hello', 'world')); // Gaunama iš podėlio
Paaiškinimas:
- Šis modelis naudoja `Map` objektą podėliui saugoti.
- `Map` leidžia naudoti bet kokio tipo duomenis kaip raktus, įskaitant objektus ir funkcijas, kas suteikia didesnį lankstumą, palyginti su paprastais JavaScript objektais.
- `Map` objekto metodai `has` ir `get` naudojami atitinkamai patikrinti ir gauti podėlyje esančias reikšmes.
4. Rekursinė Memoizacija
Memoizacija yra ypač efektyvi optimizuojant rekursines funkcijas. Išsaugodami tarpinių skaičiavimų rezultatus, galite išvengti nereikalingų skaičiavimų ir žymiai sumažinti vykdymo laiką.
function memoizeRecursive(func) {
const cache = {};
function memoized(...args) {
const key = String(args);
if (cache[key]) {
return cache[key];
} else {
cache[key] = func(memoized, ...args);
return cache[key];
}
}
return memoized;
}
// Pavyzdys: Fibonačio sekos funkcijos memoizacija
function fibonacci(memoized, n) {
if (n <= 1) {
return n;
}
return memoized(n - 1) + memoized(n - 2);
}
const memoizedFibonacci = memoizeRecursive(fibonacci);
console.time('Pirmas iškvietimas');
console.log(memoizedFibonacci(10)); // Apskaičiuoja ir išsaugo podėlyje
console.timeEnd('Pirmas iškvietimas');
console.time('Antras iškvietimas');
console.log(memoizedFibonacci(10)); // Gaunama iš podėlio
console.timeEnd('Antras iškvietimas');
Paaiškinimas:
- Funkcija `memoizeRecursive` priima funkciją `func` kaip įvesties duomenį.
- Ji sukuria `cache` objektą savo apimtyje.
- Ji grąžina naują funkciją `memoized`, kuri apgaubia pradinę funkciją.
- `memoized` funkcija patikrina, ar rezultatas su duotais argumentais jau yra podėlyje. Jei taip, grąžina išsaugotą reikšmę.
- Jei rezultato nėra podėlyje, ji iškviečia pradinę funkciją, perduodama pačią `memoized` funkciją kaip pirmąjį argumentą. Tai leidžia pradinei funkcijai rekursiškai iškviesti memoizuotą savo pačios versiją.
- Rezultatas tada išsaugomas podėlyje ir grąžinamas.
5. Klasėmis Pagrįsta Memoizacija
Objektiniame programavime memoizacija gali būti įgyvendinta klasės viduje, siekiant išsaugoti metodų rezultatus. Tai gali būti naudinga kompiuteriškai brangiems metodams, kurie dažnai iškviečiami su tais pačiais argumentais.
class MemoizedClass {
constructor() {
this.cache = {};
}
memoizeMethod(func) {
return (...args) => {
const key = JSON.stringify(args);
if (this.cache[key]) {
return this.cache[key];
} else {
const result = func.apply(this, args);
this.cache[key] = result;
return result;
}
};
}
// Pavyzdys: Metodo, skaičiuojančio skaičiaus laipsnį, memoizacija
power(base, exponent) {
console.log('Skaičiuojamas laipsnis...');
return Math.pow(base, exponent);
}
}
const memoizedInstance = new MemoizedClass();
const memoizedPower = memoizedInstance.memoizeMethod(memoizedInstance.power);
console.log(memoizedPower(2, 3)); // Apskaičiuoja ir išsaugo podėlyje
console.log(memoizedPower(2, 3)); // Gaunama iš podėlio
Paaiškinimas:
- `MemoizedClass` savo konstruktoriuje apibrėžia `cache` savybę.
- Metodas `memoizeMethod` priima funkciją kaip įvestį ir grąžina memoizuotą tos funkcijos versiją, saugodamas rezultatus klasės `cache`.
- Tai leidžia selektyviai memoizuoti konkrečius klasės metodus.
Spartinimo Strategijos
Be bazinių memoizacijos modelių, galima taikyti skirtingas spartinimo strategijas, siekiant optimizuoti podėlio elgseną ir valdyti jo dydį. Šios strategijos padeda užtikrinti, kad podėlis išliktų efektyvus ir nenaudotų per daug atminties.
1. Mažiausiai Neseniai Naudotų (LRU) Podėlis
LRU podėlis pašalina mažiausiai neseniai naudotus elementus, kai podėlis pasiekia savo maksimalų dydį. Ši strategija užtikrina, kad dažniausiai naudojami duomenys išlieka podėlyje, o rečiau naudojami duomenys yra atmetami.
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.cache = new Map();
}
get(key) {
if (this.cache.has(key)) {
const value = this.cache.get(key);
this.cache.delete(key); // Perkeliamas į pabaigą, kad būtų pažymėtas kaip neseniai naudotas
this.cache.set(key, value);
return value;
} else {
return undefined;
}
}
put(key, value) {
if (this.cache.has(key)) {
this.cache.delete(key);
}
this.cache.set(key, value);
if (this.cache.size > this.capacity) {
// Pašalinamas mažiausiai neseniai naudotas elementas
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
}
}
// Naudojimo pavyzdys:
const lruCache = new LRUCache(3); // Talpa 3
lruCache.put('a', 1);
lruCache.put('b', 2);
lruCache.put('c', 3);
console.log(lruCache.get('a')); // 1 (perkelia 'a' į galą)
lruCache.put('d', 4); // 'b' yra pašalinamas
console.log(lruCache.get('b')); // undefined
console.log(lruCache.get('a')); // 1
console.log(lruCache.get('c')); // 3
console.log(lruCache.get('d')); // 4
Paaiškinimas:
- Naudoja `Map` podėliui saugoti, kuris išlaiko įterpimo tvarką.
- `get(key)` gauna reikšmę ir iš naujo įterpia rakto-reikšmės porą, kad pažymėtų ją kaip neseniai naudotą.
- `put(key, value)` įterpia rakto-reikšmės porą. Jei podėlis yra pilnas, pašalinamas mažiausiai neseniai naudotas elementas (pirmasis elementas `Map`).
2. Mažiausiai Dažnai Naudotų (LFU) Podėlis
LFU podėlis pašalina mažiausiai dažnai naudotus elementus, kai podėlis yra pilnas. Ši strategija teikia pirmenybę dažniau naudojamiems duomenims, užtikrindama, kad jie išliktų podėlyje.
class LFUCache {
constructor(capacity) {
this.capacity = capacity;
this.cache = new Map();
this.frequencies = new Map();
this.minFrequency = 0;
}
get(key) {
if (!this.cache.has(key)) {
return undefined;
}
const frequency = this.frequencies.get(key);
this.frequencies.set(key, frequency + 1);
return this.cache.get(key);
}
put(key, value) {
if (this.capacity <= 0) {
return;
}
if (this.cache.has(key)) {
this.cache.set(key, value);
this.get(key);
return;
}
if (this.cache.size >= this.capacity) {
this.evict();
}
this.cache.set(key, value);
this.frequencies.set(key, 1);
this.minFrequency = 1;
}
evict() {
let minFreq = Infinity;
for (const frequency of this.frequencies.values()) {
minFreq = Math.min(minFreq, frequency);
}
const keysToRemove = [];
this.frequencies.forEach((freq, key) => {
if (freq === minFreq) {
keysToRemove.push(key);
}
});
const keyToRemove = keysToRemove[0];
this.cache.delete(keyToRemove);
this.frequencies.delete(keyToRemove);
}
}
// Naudojimo pavyzdys:
const lfuCache = new LFUCache(2);
lfuCache.put('a', 1);
lfuCache.put('b', 2);
console.log(lfuCache.get('a')); // 1, dažnumas(a) = 2
lfuCache.put('c', 3); // pašalina 'b', nes dažnumas(b) = 1
console.log(lfuCache.get('b')); // undefined
console.log(lfuCache.get('a')); // 1, dažnumas(a) = 3
console.log(lfuCache.get('c')); // 3, dažnumas(c) = 2
Paaiškinimas:
- Naudoja du `Map` objektus: `cache` rakto-reikšmės poroms saugoti ir `frequencies` kiekvieno rakto naudojimo dažnumui saugoti.
- `get(key)` gauna reikšmę ir padidina dažnumo skaitiklį.
- `put(key, value)` įterpia rakto-reikšmės porą. Jei podėlis yra pilnas, jis pašalina mažiausiai dažnai naudotą elementą.
- `evict()` suranda mažiausią dažnumo skaičių ir pašalina atitinkamą rakto-reikšmės porą iš abiejų – `cache` ir `frequencies`.
3. Laiku Pagrįstas Galiojimo Laikas
Ši strategija panaikina podėlyje esančius elementus po tam tikro laiko. Tai naudinga duomenims, kurie laikui bėgant tampa pasenę. Pavyzdžiui, spartinant API atsakymus, kurie galioja tik kelias minutes.
function memoizeWithExpiration(func, ttl) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
const cached = cache.get(key);
if (cached && cached.expiry > Date.now()) {
return cached.value;
} else {
const result = func.apply(this, args);
cache.set(key, { value: result, expiry: Date.now() + ttl });
return result;
}
};
}
// Pavyzdys: Funkcijos memoizacija su 5 sekundžių galiojimo laiku
function getDataFromAPI(endpoint) {
console.log(`Gaunami duomenys iš ${endpoint}...`);
// Simuliuojamas API iškvietimas su vėlavimu
return new Promise(resolve => {
setTimeout(() => {
resolve(`Duomenys iš ${endpoint}`);
}, 1000);
});
}
const memoizedGetData = memoizeWithExpiration(getDataFromAPI, 5000); // TTL: 5 sekundės
async function testExpiration() {
console.log(await memoizedGetData('/users')); // Gauna ir išsaugo podėlyje
console.log(await memoizedGetData('/users')); // Gaunama iš podėlio
setTimeout(async () => {
console.log(await memoizedGetData('/users')); // Gauna iš naujo po 5 sekundžių
}, 6000);
}
testExpiration();
Paaiškinimas:
- Funkcija `memoizeWithExpiration` priima funkciją `func` ir galiojimo laiką (TTL) milisekundėmis kaip įvesties duomenis.
- Ji saugo podėlio reikšmę kartu su galiojimo laiko žyme.
- Prieš grąžindama podėlyje esančią reikšmę, ji patikrina, ar galiojimo laiko žyma dar nėra praėjusi. Jei ne, ji panaikina podėlio įrašą ir iš naujo gauna duomenis.
Našumo Augimas ir Svarstymai
Memoizacija gali žymiai pagerinti našumą, ypač kompiuteriškai brangioms funkcijoms, kurios nuolat iškviečiamos su tais pačiais įvesties duomenimis. Našumo augimas labiausiai pastebimas šiais atvejais:
- Rekursinės funkcijos: Memoizacija gali dramatiškai sumažinti rekursinių iškvietimų skaičių, vedant prie eksponentinio našumo pagerėjimo.
- Funkcijos su persidengiančiomis subproblemomis: Memoizacija gali išvengti nereikalingų skaičiavimų, saugodama subproblemų rezultatus ir juos pakartotinai naudodama prireikus.
- Funkcijos su dažnais identiškais įvesties duomenimis: Memoizacija užtikrina, kad funkcija bus vykdoma tik vieną kartą kiekvienam unikaliam įvesties duomenų rinkiniui.
Tačiau svarbu atsižvelgti į šiuos kompromisus, naudojant memoizaciją:
- Atminties suvartojimas: Memoizacija padidina atminties naudojimą, nes saugo funkcijų iškvietimų rezultatus. Tai gali kelti susirūpinimą funkcijoms su dideliu galimų įvesties duomenų skaičiumi arba programoms su ribotais atminties resursais.
- Podėlio anuliavimas: Jei pagrindiniai duomenys pasikeičia, podėlyje esantys rezultatai gali tapti pasenę. Būtina įgyvendinti podėlio anuliavimo strategiją, siekiant užtikrinti, kad podėlis išliktų suderinamas su duomenimis.
- Sudėtingumas: Memoizacijos įgyvendinimas gali pridėti kodo sudėtingumo, ypač sudėtingoms spartinimo strategijoms. Svarbu atidžiai apsvarstyti kodo sudėtingumą ir palaikomumą prieš naudojant memoizaciją.
Praktiniai Pavyzdžiai ir Panaudojimo Atvejai
Memoizacija gali būti taikoma įvairiausiose situacijose siekiant optimizuoti našumą. Štai keletas praktinių pavyzdžių:
- Front-end interneto kūrimas: Brangių skaičiavimų memoizacija JavaScript kalboje gali pagerinti interneto programų reakcijos laiką. Pavyzdžiui, galite memoizuoti funkcijas, atliekančias sudėtingas DOM manipuliacijas arba skaičiuojančias išdėstymo savybes.
- Serverio pusės programos: Memoizacija gali būti naudojama spartinti duomenų bazių užklausų ar API iškvietimų rezultatus, sumažinant serverio apkrovą ir pagerinant atsakymo laikus.
- Duomenų analizė: Memoizacija gali pagreitinti duomenų analizės užduotis, išsaugant tarpinių skaičiavimų rezultatus. Pavyzdžiui, galite memoizuoti funkcijas, atliekančias statistinę analizę ar mašininio mokymosi algoritmus.
- Žaidimų kūrimas: Memoizacija gali būti naudojama optimizuoti žaidimų našumą, išsaugant dažnai naudojamų skaičiavimų, tokių kaip susidūrimų aptikimas ar kelio paieška, rezultatus.
Išvada
Memoizacija yra galinga optimizavimo technika, kuri gali žymiai pagerinti JavaScript programų našumą. Išsaugodami brangių funkcijų iškvietimų rezultatus, galite išvengti nereikalingų skaičiavimų ir sumažinti vykdymo laiką. Tačiau svarbu atidžiai apsvarstyti kompromisus tarp našumo augimo ir atminties suvartojimo, podėlio anuliavimo ir kodo sudėtingumo. Suprasdami skirtingus memoizacijos modelius ir spartinimo strategijas, galite efektyviai taikyti memoizaciją, kad optimizuotumėte savo JavaScript kodą ir kurtumėte didelio našumo programas.