Uurige JavaScripti memoiseerimistehnikaid, vahemällu salvestamise strateegiaid ja praktilisi näiteid koodi jõudluse optimeerimiseks. Õppige memoiseerimismustreid kiiremaks täitmiseks.
JavaScripti memoiseerimismustrid: vahemällu salvestamise strateegiad ja jõudluse kasv
Tarkvaraarenduse valdkonnas on jõudlus esmatähtis. JavaScript, olles mitmekülgne keel, mida kasutatakse erinevates keskkondades alates veebi esiotsa arendusest kuni serveripoolsete rakendusteni Node.js-iga, nõuab sageli optimeerimist, et tagada sujuv ja tõhus täitmine. Üks võimas tehnika, mis võib teatud stsenaariumides jõudlust oluliselt parandada, on memoiseerimine.
Memoiseerimine on optimeerimistehnika, mida kasutatakse peamiselt arvutiprogrammide kiirendamiseks, salvestades kulukate funktsioonikutsete tulemused ja tagastades vahemällu salvestatud tulemuse, kui samad sisendid uuesti esinevad. Sisuliselt on see vahemällu salvestamise vorm, mis on suunatud spetsiifiliselt funktsioonidele. See lähenemine on eriti tõhus funktsioonide puhul, mis on:
- Puhtad: Funktsioonid, mille tagastusväärtus sõltub ainult nende sisendväärtustest, ilma kõrvalmõjudeta.
- Deterministlikud: Sama sisendi korral annab funktsioon alati sama väljundi.
- Kulukaid: Funktsioonid, mille arvutused on arvutusmahukad või aeganõudvad (nt rekursiivsed funktsioonid, keerulised arvutused).
See artikkel uurib memoiseerimise kontseptsiooni JavaScriptis, süvenedes erinevatesse mustritesse, vahemällu salvestamise strateegiatesse ja jõudluse kasvu, mida selle rakendamisega on võimalik saavutada. Vaatleme praktilisi näiteid, et illustreerida, kuidas memoiseerimist erinevates stsenaariumides tõhusalt rakendada.
Memoiseerimise mõistmine: põhikontseptsioon
Oma tuumas kasutab memoiseerimine vahemällu salvestamise põhimõtet. Kui memoiseeritud funktsiooni kutsutakse välja kindla argumentide komplektiga, kontrollib see esmalt, kas nende argumentide tulemus on juba arvutatud ja salvestatud vahemällu (tavaliselt JavaScripti objekt või Map). Kui tulemus leitakse vahemälust, tagastatakse see kohe. Vastasel juhul teostab funktsioon arvutuse, salvestab tulemuse vahemällu ja seejärel tagastab selle.
Peamine eelis seisneb üleliigsete arvutuste vältimises. Kui funktsiooni kutsutakse mitu korda samade sisenditega, teostab memoiseeritud versioon arvutuse ainult üks kord. Järgnevad kutsed hangivad tulemuse otse vahemälust, mis toob kaasa märkimisväärse jõudluse paranemise, eriti arvutusmahukate operatsioonide puhul.
Memoiseerimismustrid JavaScriptis
JavaScriptis saab memoiseerimise rakendamiseks kasutada mitmeid mustreid. Vaatleme mõningaid kõige levinumaid ja tõhusamaid:
1. Põhiline memoiseerimine sulundiga
See on kõige fundamentaalsem lähenemine memoiseerimisele. See kasutab sulundit (closure), et hoida vahemälu funktsiooni skoobis. Vahemälu on tavaliselt lihtne JavaScripti objekt, kus võtmed esindavad funktsiooni argumente ja väärtused vastavaid tulemusi.
function memoize(func) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args); // Loo argumentidele unikaalne võti
if (cache[key]) {
return cache[key]; // Tagasta vahemällu salvestatud tulemus
} else {
const result = func.apply(this, args); // Arvuta tulemus
cache[key] = result; // Salvesta tulemus vahemällu
return result; // Tagasta tulemus
}
};
}
// Näide: faktoriaali funktsiooni memoiseerimine
function factorial(n) {
if (n <= 1) {
return 1;
}
return n * factorial(n - 1);
}
const memoizedFactorial = memoize(factorial);
console.time('Esimene kutse');
console.log(memoizedFactorial(5)); // Arvutab ja salvestab vahemällu
console.timeEnd('Esimene kutse');
console.time('Teine kutse');
console.log(memoizedFactorial(5)); // Hangib vahemälust
console.timeEnd('Teine kutse');
Selgitus:
- Funktsioon `memoize` võtab sisendiks funktsiooni `func`.
- See loob oma skoobis (kasutades sulundit) `cache` objekti.
- See tagastab uue funktsiooni, mis mähkib algse funktsiooni.
- See mähkiv funktsioon loob unikaalse võtme funktsiooni argumentide põhjal, kasutades `JSON.stringify(args)`.
- See kontrollib, kas `key` eksisteerib `cache`-s. Kui jah, siis tagastab see vahemällu salvestatud väärtuse.
- Kui `key` ei eksisteeri, kutsub see välja algse funktsiooni, salvestab tulemuse `cache`-i ja tagastab tulemuse.
Piirangud:
- `JSON.stringify` võib olla keeruliste objektide puhul aeglane.
- Võtme loomine võib olla problemaatiline funktsioonidega, mis aktsepteerivad argumente erinevas järjekorras või mis on samade võtmetega, kuid erineva järjestusega objektid.
- Ei käsitle `NaN` korrektselt, kuna `JSON.stringify(NaN)` tagastab `null`.
2. Memoiseerimine kohandatud võtme generaatoriga
`JSON.stringify` piirangute lahendamiseks saate luua kohandatud võtme generaatori funktsiooni, mis toodab unikaalse võtme funktsiooni argumentide põhjal. See annab rohkem kontrolli selle üle, kuidas vahemälu indekseeritakse ja võib teatud stsenaariumides parandada jõudlust.
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;
}
};
}
// Näide: kahe arvu liitmise funktsiooni memoiseerimine
function add(a, b) {
console.log('Arvutan...');
return a + b;
}
// Kohandatud võtme generaator liitmise funktsioonile
function addKeyGenerator(a, b) {
return `${a}-${b}`;
}
const memoizedAdd = memoizeWithKey(add, addKeyGenerator);
console.log(memoizedAdd(2, 3)); // Arvutab ja salvestab vahemällu
console.log(memoizedAdd(2, 3)); // Hangib vahemälust
console.log(memoizedAdd(3, 2)); // Arvutab ja salvestab vahemällu (erinev võti)
Selgitus:
- See muster sarnaneb põhilise memoiseerimisega, kuid see aktsepteerib lisargumenti: `keyGenerator`.
- `keyGenerator` on funktsioon, mis võtab samu argumente kui algne funktsioon ja tagastab unikaalse võtme.
- See võimaldab paindlikumat ja tõhusamat võtme loomist, eriti funktsioonide puhul, mis töötavad keeruliste andmestruktuuridega.
3. Memoiseerimine Map-iga
`Map`-objekt JavaScriptis pakub robustsemat ja mitmekülgsemat viisi vahemällu salvestatud tulemuste hoidmiseks. Erinevalt tavalistest JavaScripti objektidest võimaldab `Map` kasutada võtmetena mis tahes andmetüüpi, sealhulgas objekte ja funktsioone. See välistab vajaduse argumentide stringimiseks ja lihtsustab võtme loomist.
function memoizeWithMap(func) {
const cache = new Map();
return function(...args) {
const key = args.join('|'); // Loo lihtne võti (võib olla keerulisem)
if (cache.has(key)) {
return cache.get(key);
} else {
const result = func.apply(this, args);
cache.set(key, result);
return result;
}
};
}
// Näide: stringide ühendamise funktsiooni memoiseerimine
function concatenate(str1, str2) {
console.log('Ăśhendan...');
return str1 + str2;
}
const memoizedConcatenate = memoizeWithMap(concatenate);
console.log(memoizedConcatenate('tere', 'maailm')); // Arvutab ja salvestab vahemällu
console.log(memoizedConcatenate('tere', 'maailm')); // Hangib vahemälust
Selgitus:
- See muster kasutab vahemälu hoidmiseks `Map` objekti.
- `Map` võimaldab kasutada võtmetena mis tahes andmetüüpi, sealhulgas objekte ja funktsioone, mis pakub suuremat paindlikkust võrreldes tavaliste JavaScripti objektidega.
- Vahemällu salvestatud väärtuste kontrollimiseks ja hankimiseks kasutatakse `Map` objekti meetodeid `has` ja `get`.
4. Rekursiivne memoiseerimine
Memoiseerimine on eriti tõhus rekursiivsete funktsioonide optimeerimiseks. Vahetulemuste vahemällu salvestamisega saate vältida üleliigseid arvutusi ja oluliselt vähendada täitmisaega.
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;
}
// Näide: Fibonacci jada funktsiooni memoiseerimine
function fibonacci(memoized, n) {
if (n <= 1) {
return n;
}
return memoized(n - 1) + memoized(n - 2);
}
const memoizedFibonacci = memoizeRecursive(fibonacci);
console.time('Esimene kutse');
console.log(memoizedFibonacci(10)); // Arvutab ja salvestab vahemällu
console.timeEnd('Esimene kutse');
console.time('Teine kutse');
console.log(memoizedFibonacci(10)); // Hangib vahemälust
console.timeEnd('Teine kutse');
Selgitus:
- Funktsioon `memoizeRecursive` võtab sisendiks funktsiooni `func`.
- See loob oma skoobis `cache` objekti.
- See tagastab uue funktsiooni `memoized`, mis mähkib algse funktsiooni.
- `memoized` funktsioon kontrollib, kas antud argumentide tulemus on juba vahemälus. Kui on, tagastab see vahemällu salvestatud väärtuse.
- Kui tulemust pole vahemälus, kutsub see välja algse funktsiooni, andes esimese argumendina kaasa `memoized` funktsiooni enda. See võimaldab algsel funktsioonil rekursiivselt kutsuda iseenda memoiseeritud versiooni.
- Seejärel salvestatakse tulemus vahemällu ja tagastatakse.
5. Klassipõhine memoiseerimine
Objektorienteeritud programmeerimises saab memoiseerimist rakendada klassi sees, et salvestada meetodite tulemusi vahemällu. See võib olla kasulik arvutusmahukate meetodite puhul, mida kutsutakse sageli samade argumentidega.
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;
}
};
}
// Näide: arvu astendamise meetodi memoiseerimine
power(base, exponent) {
console.log('Arvutan astet...');
return Math.pow(base, exponent);
}
}
const memoizedInstance = new MemoizedClass();
const memoizedPower = memoizedInstance.memoizeMethod(memoizedInstance.power);
console.log(memoizedPower(2, 3)); // Arvutab ja salvestab vahemällu
console.log(memoizedPower(2, 3)); // Hangib vahemälust
Selgitus:
- `MemoizedClass` defineerib oma konstruktoris `cache` omaduse.
- `memoizeMethod` võtab sisendiks funktsiooni ja tagastab selle funktsiooni memoiseeritud versiooni, salvestades tulemused klassi `cache`-i.
- See võimaldab teil valikuliselt memoiseerida klassi konkreetseid meetodeid.
Vahemällu salvestamise strateegiad
Lisaks põhilistele memoiseerimismustritele saab vahemälu käitumise optimeerimiseks ja selle suuruse haldamiseks kasutada erinevaid vahemällu salvestamise strateegiaid. Need strateegiad aitavad tagada, et vahemälu jääb tõhusaks ega tarbi liigselt mälu.
1. Vähim hiljuti kasutatud (LRU) vahemälu
LRU vahemälu eemaldab vähim hiljuti kasutatud elemendid, kui vahemälu saavutab oma maksimaalse suuruse. See strateegia tagab, et kõige sagedamini kasutatavad andmed jäävad vahemällu, samas kui harvemini kasutatavad andmed visatakse ära.
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); // Sisesta uuesti, et märkida hiljuti kasutatuks
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) {
// Eemalda vähim hiljuti kasutatud element
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
}
}
// Kasutusnäide:
const lruCache = new LRUCache(3); // Mahtuvus 3
lruCache.put('a', 1);
lruCache.put('b', 2);
lruCache.put('c', 3);
console.log(lruCache.get('a')); // 1 (liigutab 'a' lõppu)
lruCache.put('d', 4); // 'b' eemaldatakse
console.log(lruCache.get('b')); // undefined
console.log(lruCache.get('a')); // 1
console.log(lruCache.get('c')); // 3
console.log(lruCache.get('d')); // 4
Selgitus:
- Kasutab vahemälu hoidmiseks `Map`-i, mis säilitab sisestamise järjekorra.
- `get(key)` hangib väärtuse ja sisestab võtme-väärtuse paari uuesti, et märkida see hiljuti kasutatuks.
- `put(key, value)` sisestab võtme-väärtuse paari. Kui vahemälu on täis, eemaldatakse vähim hiljuti kasutatud element (esimene element `Map`-is).
2. Vähim sagedamini kasutatud (LFU) vahemälu
LFU vahemälu eemaldab vähim sagedamini kasutatud elemendid, kui vahemälu on täis. See strateegia eelistab andmeid, millele pääsetakse sagedamini juurde, tagades, et need jäävad vahemällu.
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);
}
}
// Kasutusnäide:
const lfuCache = new LFUCache(2);
lfuCache.put('a', 1);
lfuCache.put('b', 2);
console.log(lfuCache.get('a')); // 1, sagedus(a) = 2
lfuCache.put('c', 3); // eemaldab 'b', sest sagedus(b) = 1
console.log(lfuCache.get('b')); // undefined
console.log(lfuCache.get('a')); // 1, sagedus(a) = 3
console.log(lfuCache.get('c')); // 3, sagedus(c) = 2
Selgitus:
- Kasutab kahte `Map` objekti: `cache` võtme-väärtuse paaride hoidmiseks ja `frequencies` iga võtme kasutussageduse hoidmiseks.
- `get(key)` hangib väärtuse ja suurendab sagedusloendurit.
- `put(key, value)` sisestab võtme-väärtuse paari. Kui vahemälu on täis, eemaldab see vähim sagedamini kasutatud elemendi.
- `evict()` leiab minimaalse sagedusloenduri ja eemaldab vastava võtme-väärtuse paari nii `cache`-ist kui ka `frequencies`-ist.
3. Ajapõhine aegumine
See strateegia muudab vahemällu salvestatud elemendid kehtetuks pärast teatud aja möödumist. See on kasulik andmete puhul, mis muutuvad aja jooksul vananenuks. Näiteks API vastuste vahemällu salvestamine, mis on kehtivad vaid mõne minuti.
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;
}
};
}
// Näide: funktsiooni memoiseerimine 5-sekundilise aegumisajaga
function getDataFromAPI(endpoint) {
console.log(`Andmete pärimine aadressilt ${endpoint}...`);
// Simuleeri API-kutset viivitusega
return new Promise(resolve => {
setTimeout(() => {
resolve(`Andmed aadressilt ${endpoint}`);
}, 1000);
});
}
const memoizedGetData = memoizeWithExpiration(getDataFromAPI, 5000); // TTL: 5 sekundit
async function testExpiration() {
console.log(await memoizedGetData('/users')); // Pärib ja salvestab vahemällu
console.log(await memoizedGetData('/users')); // Hangib vahemälust
setTimeout(async () => {
console.log(await memoizedGetData('/users')); // Pärib uuesti 5 sekundi pärast
}, 6000);
}
testExpiration();
Selgitus:
- Funktsioon `memoizeWithExpiration` võtab sisendiks funktsiooni `func` ja eluea (TTL) väärtuse millisekundites.
- See salvestab vahemällu väärtuse koos aegumise ajatempliga.
- Enne vahemälust väärtuse tagastamist kontrollib see, kas aegumise ajatempel on veel tulevikus. Kui ei, siis muudab see vahemälu kehtetuks ja pärib andmed uuesti.
Jõudluse kasv ja kaalutlused
Memoiseerimine võib oluliselt parandada jõudlust, eriti arvutusmahukate funktsioonide puhul, mida kutsutakse korduvalt samade sisenditega. Jõudluse kasv on kõige märgatavam järgmistes stsenaariumides:
- Rekursiivsed funktsioonid: Memoiseerimine võib dramaatiliselt vähendada rekursiivsete kutsete arvu, mis toob kaasa eksponentsiaalse jõudluse paranemise.
- Funktsioonid kattuvate alamprobleemidega: Memoiseerimine võib vältida üleliigseid arvutusi, salvestades alamprobleemide tulemused ja taaskasutades neid vajadusel.
- Funktsioonid sagedaste identsete sisenditega: Memoiseerimine tagab, et funktsioon käivitatakse ainult üks kord iga unikaalse sisendite komplekti jaoks.
Siiski on oluline arvestada järgmiste kompromissidega memoiseerimise kasutamisel:
- Mälukasutus: Memoiseerimine suurendab mälukasutust, kuna see salvestab funktsioonikutsete tulemusi. See võib olla probleemiks funktsioonide puhul, millel on suur hulk võimalikke sisendeid, või piiratud mälumahtudega rakenduste puhul.
- Vahemälu kehtetuks muutmine: Kui alusandmed muutuvad, võivad vahemällu salvestatud tulemused vananeda. On ülioluline rakendada vahemälu kehtetuks muutmise strateegia, et tagada vahemälu vastavus andmetega.
- Keerukus: Memoiseerimise rakendamine võib lisada koodile keerukust, eriti keeruliste vahemällu salvestamise strateegiate puhul. Enne memoiseerimise kasutamist on oluline hoolikalt kaaluda koodi keerukust ja hooldatavust.
Praktilised näited ja kasutusjuhud
Memoiseerimist saab rakendada laias valikus stsenaariumides jõudluse optimeerimiseks. Siin on mõned praktilised näited:
- Veebi esiotsa arendus: Kulukate arvutuste memoiseerimine JavaScriptis võib parandada veebirakenduste reageerimisvõimet. Näiteks saate memoiseerida funktsioone, mis teostavad keerulisi DOM-manipulatsioone või arvutavad paigutuse omadusi.
- Serveripoolsed rakendused: Memoiseerimist saab kasutada andmebaasipäringute või API-kutsete tulemuste vahemällu salvestamiseks, vähendades serveri koormust ja parandades vastuseaegu.
- Andmeanalüüs: Memoiseerimine võib kiirendada andmeanalüüsi ülesandeid, salvestades vahetulemusi vahemällu. Näiteks saate memoiseerida funktsioone, mis teostavad statistilist analüüsi või masinõppe algoritme.
- Mänguarendus: Memoiseerimist saab kasutada mängu jõudluse optimeerimiseks, salvestades sageli kasutatavate arvutuste tulemusi, nagu kokkupõrke tuvastamine või teekonna leidmine.
Kokkuvõte
Memoiseerimine on võimas optimeerimistehnika, mis võib märkimisväärselt parandada JavaScripti rakenduste jõudlust. Kulukate funktsioonikutsete tulemuste vahemällu salvestamisega saate vältida üleliigseid arvutusi ja vähendada täitmisaega. Siiski on oluline hoolikalt kaaluda kompromisse jõudluse kasvu ja mälukasutuse, vahemälu kehtetuks muutmise ja koodi keerukuse vahel. Mõistes erinevaid memoiseerimismustreid ja vahemällu salvestamise strateegiaid, saate memoiseerimist tõhusalt rakendada oma JavaScripti koodi optimeerimiseks ja suure jõudlusega rakenduste loomiseks.