7.3. Демонстрация - Кешируем с помощью Map
Шаг 1
Самая частая проблема для использования Map — кеширование. Про него мы уже говорили во втором разделе, когда познакомились с Map. Теперь пришло время и самим написать кеш! Напомним, что кеш — это некая структура данных, с помощью которой мы «запоминаем» результаты выполнения долгих функций (запрос с API или просто тяжёлые вычисления) и потом переиспользуем их вместо того, чтобы заново всё высчитывать. Было бы хорошо, чтобы кеш работал за O(1) на чтение и запись (что Map нам и позволяет сделать). Писать будем так называемый LRU-кеш или Least Recently Used кеш. Суть его в том, чтобы запоминать только последние несколько записей (сколько — дело нашей настройки), а как только в него пытаются записать больше — удалять самую старую. Такой кеш хорошо подходит для случаев, когда параметров может быть очень много, а памяти мы хотим или можем выделять только определённое количество.
class Cache {
constructor(size) {
// размер кеша в ключах, которые он может сохранить.
this.size = size;
}
// Получение из кеша по ключу
// (обычно ключом как раз выступает аргумент функции, которую мы хотим кешировать)
get(key) {
}
// Запись в кеш
set(key, value) {
}
// Удаление по ключу, если вдруг нам это пригодится
delete(key) {
}
}
Шаг 2
Для начала создадим непосредственно хеш-таблицу и опишем самые простые методы. Описание удаления и чтения из кеша очень просты — это всего лишь «обёртки» над встроенными в Map функциями.
class Cache {
constructor(size) {
// размер кеша в ключах, которые он может сохранить.
this.size = size;
this._map = new Map();
}
// Получение из кеша по ключу
// (обычно ключом как раз выступает аргумент функции, которую мы хотим кешировать)
get(key) {
return this._map.get(key);
}
// Запись в кеш
set(key, value) {
}
// Удаление по ключу, если вдруг нам это пригодится
delete(key) {
this._map.delete(key);
}
}
Шаг 3
А вот с заполнением кеша не всё так просто — хотя с реализацией Map в JavaScript нам повезло больше, чем другим языкам. Перед тем, как положить новый ключ в таблицу, нам нужно убедиться, что она не переполнена. И если так есть, то удалить самый первый ключ. К счастью, Map хранит ключи итератором (получаемым методом keys()) в порядке добавления, а размер — отдельным полем size. Так что нам не придётся мучиться ни с поддержкой собственного счётчика размера, ни с организацией ключей в самописный связный список (хотя это мы уже можем делать).
class Cache {
constructor(size) {
// размер кеша в ключах, которые он может сохранить.
this.size = size;
this._map = new Map();
}
// Получение из кеша по ключу
// (обычно ключом как раз выступает аргумент функции, которую мы хотим кешировать)
get(key) {
return this._map.get(key);
}
// Запись в кеш
set(key, value) {
if (this._map.size === this.size) {
this.delete(this._first());
}
this._map.set(key, value);
}
_first() {
return this._map.keys().next().value;
}
// Удаление по ключу, если вдруг нам это пригодится
delete(key) {
this._map.delete(key);
}
}
Шаг 4
Можно убедиться в работоспособности нашего кеша на «игрушечных данных», но на самом деле он помогает решить настоящие проблемы.
import {logDelete, logSet, logGet} from './logger';
class Cache {
constructor(size) {
// размер кеша в ключах, которые он может сохранить.
this.size = size;
this._map = new Map();
}
// Получение из кеша по ключу
// (обычно ключом как раз выступает аргумент функции, которую мы хотим кешировать)
get(key) {
const value = this._map.get(key);
logGet(key, value);
return value;
}
// Запись в кеш
set(key, value) {
if (this._map.size === this.size) {
this.delete(this._first());
}
logSet(key, value);
this._map.set(key, value);
}
_first() {
return this._map.keys().next().value;
}
// Удаление по ключу, если вдруг нам это пригодится
delete(key) {
logDelete(key);
this._map.delete(key);
}
}
const cache = new Cache(2);
cache.set(1, 'foo');
cache.set(2, 'bar');
cache.get(1); // foo
cache.set(3, 'baz');
cache.get(1); // undefined
Шаг 5
Одним из частых применений кеша, помимо кеширования ответов запросов с API, является кеширование результата функций «Разделяй и властвуй». Как правило, они «скатываются» если не к одним и тем же, то к очень похожим случаям. Можно, например, написать гораздо более быструю реализацию функции для получения чисел Фибоначчи, просто скопировав её рекурсивную версию и внедрив в неё кеш! Посмотрите на сравнение быстродействия нашей старой функции для подсчета Фибоначчи и её же, но с кешем!
import {benchmark} from './benchmarker';
class Cache {
constructor(size) {
// размер кеша в ключах, которые он может сохранить.
this.size = size;
this._map = new Map();
}
// Получение из кеша по ключу
// (обычно ключом как раз выступает аргумент функции, которую мы хотим кешировать)
get(key) {
return this._map.get(key);
}
// Запись в кеш
set(key, value) {
if (this._map.size === this.size) {
this.delete(this._first());
}
this._map.set(key, value);
}
_first() {
return this._map.keys().next().value;
}
// Удаление по ключу, если вдруг нам это пригодится
delete(key) {
this._map.delete(key);
}
}
function cachedFibonacci() {
const cache = new Cache(300);
return function fibonacci(index) {
if (index < 1 || !Number.isInteger(index)) {
throw new Error("Индекс должен быть целым числом больше или равным единице!");
}
if (index === 1) {
return 0;
}
if (index === 2) {
return 1;
}
const cached = cache.get(index);
if (cached) {
return cached;
}
const calculated = fibonacci(index - 1) + fibonacci(index - 2);
cache.set(index, calculated);
return calculated;
}
}
const fibonacci = cachedFibonacci();
function uncachedFibonacci(index) {
if (index < 1 || !Number.isInteger(index)) {
throw new Error("Индекс должен быть целым числом больше или равным единице!");
}
if (index === 1) {
return 0;
}
if (index === 2) {
return 1;
}
return uncachedFibonacci(index - 1) + uncachedFibonacci(index - 2);
}
benchmark(() => fibonacci(300));
benchmark(() => uncachedFibonacci(42));
File benchmarker.js
const results = document.getElementById('results')
export function benchmark(callback) {
const start = performance.now();
const result = callback();
const end = performance.now();
const time = (end - start) / 1000;
log(`Получили ${result} за ${time.toFixed(5)} секунд`);
}
function log(message) {
const result = document.createElement('li');
result.innerHTML = message;
results.appendChild(result);
}
File logger.js
const results = document.getElementById('results')
export function logDelete(key) {
log(`Удалили ключ ${key}`);
}
export function logSet(key, value) {
log(`Сохранили ${value} по ключу ${key}`);
}
export function logGet(key, value) {
log(`Получили ${value} по ключу ${key}`);
}
function log(message) {
const result = document.createElement('li');
result.innerHTML = message;
results.appendChild(result);
}