Na czym polega memoizacja?
Memoizacja to technika optymalizacyjna, stosowana głównie, aby poprawić szybkość działania programu komputerowego poprzez zapamiętywanie rezultatu wywołań funkcji i zwracanie zapamiętanej (w żargonie IT: zkeszowanej od eng. cache) wartości, gdy argumenty funkcji się nie zmieniły.
Kiedy używać memoizacji? - przykłady
Techniki memoizacji warto użyć, gdy funkcja, której rezultat chcemy zapamiętać (w żargonie IT: zkeszować, zmemoizować):
- jest tzw. “pure function”, czyli funkcją, która nie zawiera side effect’ów, więc dla danego X zawsze zwraca taki sam rezultat, TUTAJ jest super wyjaśnienie czym są pure functions,
- zawiera kosztowne obliczenia, które potrzebują sporo czasu i zasobów, aby zwrócić ów rezultat,
- wykonuje zapytania do API. Jeśli argument wywołania tej funkcji jest taki sam jak przy poprzednim wywołaniu, to rezultat zostanie zwrócony z pamięci (cache),
- w przypadku funkcji, które wywołują same siebie z powtarzającymi się argumentami, np. funkcję rekursywne.
Memoizacja - prosty przykład implementacji
Zerknij na poniższy przykład kodu i zwróć uwagę na komentarze, które wyjaśniają co się dokładnie dzieje. Jest to prosta funkcja, która podaną liczbę dzieli zawsze razy 9. Kluczowe jest tutaj słowo zawsze - sugeruje ono, że jest to “pure function”.
const cache = {} // stwórz obiekt pamięci - cache,
// który posłuży do przechowania zapamiętywanego rezultatu
function multiplyByNine(input) {
// sprawdź czy podana liczba "input" nie była już przypadkiem zapamiętana
if (!cache.hasOwnProperty(input)) {
// zapamiętaj rezultat mnożenia przez 9 dla podanej liczby "input"
cache[input] = input * 9
}
return cache[input] // zwróć zapamiętany rezultat
}
A tak to zadziała w praktyce (wyjaśnienie w komentarzach w kodzie):
multiplyByNine(1); // funkcja zwróci 9 jako wynik działania 1 * 9
multiplyByNine(1); // funckja zwróci 9, natomiast wynik zostanie zwrócony
// z pamięci bez ponownego wykonywania obliczeń ponieważ argument podany do funkcji był nadal taki sam
Proste i zrozumiałe, prawda?
P. S. Przykład kodu zaczerpnąłem od Kent C. Dodds’a ze strony: https://epicreact.dev/memoization-and-react/
React useMemo() - kod źródłowy, czyli jak to wygląda pod maską?
Przykładem implementacji techniki memoizacji jest React’owy hook o nazwie useMemo(). Nawet jeśli nie znasz React’a, przeanalizuj implementację tej funkcji wraz z komentarzami, a zauważysz pewne analogie do poprzedniego przykładu 🙂
function updateMemo<T>(
nextCreate: () => T, // podaj funkcję której rezultat chcesz zmemoizować
deps: Array<mixed> | void | null, // podaj tablicę zależności, których zmiany chcesz obserwować
): T {
// stwórz obiekt hook, który jest odpowiednikiem obiektu cache z poprzedniego przykładu
const hook = updateWorkInProgressHook();
// sprawdź czy podana tablica zależności istnieje, jeśli tak to zwróć te tablice, jeśli nie zwróć null
const nextDeps = deps === undefined ? null : deps;
// odczytaj poprzednio zapamiętany stan (poprzedni cache)
const prevState = hook.memoizedState;
// jeśli poprzedni zapamiętany rezultat istnieje w pamięci
if (prevState !== null) {
// jeśli tablica zależności istnieje
if (nextDeps !== null) {
// odczytaj poprzednią tablice zależności
const prevDeps: Array<mixed> | null = prevState[1];
// jeśli poprzednia i obecna tablica zależności jest taka sama
if (areHookInputsEqual(nextDeps, prevDeps)) {
// zwróć zapamiętany poprzednio rezultat z pamięci
return prevState[0];
}
}
}
// w przeciwnym przypadku
// wykonaj podaną pierwotnie funkcję i przypisz jej rezultat do nextValue
const nextValue = nextCreate();
// zapamiętaj nowy rezultat(nextValue) oraz nową tablicę zależności(nextDeps)
hook.memoizedState = [nextValue, nextDeps];
// zwróć nowy rezultat
return nextValue;
}
Jeśli przeczytałeś dokładnie komentarze, linijka po linijce to na pewno zauważyłeś analogię do przykładu opisanego wcześniej. Jeśli nie rozumiesz każdej linijki kodu pochodzącej z implementacji hooka useMemo — nie istotne. Chodziło mi tutaj tylko o to, abyś zauważył/a, zastosowanie techniki memoizacji, którą często używają na co dzień developerzy React.
PS. Dokładny kod źródłowy useMemo() znajdziesz TUTAJ
To koniec! Powinieneś teraz już rozumieć jak działa memoizacja 😀
🚨🚨🚨 UWAGA BONUS dla tych, którzy lubią wiedzieć więcej
Kiedy nie używać memoizacji?
- Nie staraj się stosować techniki memoizacji wszędzie i za wszelką cenę. Optymalizacja wydajności to koszt związany z poświęceniem Twojego czasu — jeśli aplikacja działa płynnie i nie zauważasz żadnych realnych spowolnień działania programu, to nie optymalizuj.
- Wywołanie funkcji memoizującej samo w sobie jest kosztem pod względem czasu i zasobów. To kolejny argument, poświadczający, że nie warto memoizować wszystkiego, co się da, tylko dla zasady czy własnego widzimisię.
- Nie memoizuj funkcji, które nie są “pure” i zawierają side effect’y, może to doprowadzić do niepoprawnego i nieprzewidywalnego działania programu.
Pozdrawiam i do następnego 👋