Реализация кеширования LRU в Javascript

В Java есть LinkedHashMap, которая переносит 99% в кеш LRU.

Есть ли реализация Javascript кеша LRU, желательно из авторитетного источника, а именно:

  1. понятно
  2. эффективный (амортизированный O (1) получить / положить / удалить)

? Я искал в Интернете, но не нашел; Я думал, что нашел его на шаблонах проектирования Ajax, но он затушевывает метод sendToTail() и имеет производительность O (n) (предположительно, поскольку очередь и ассоциативный массив разделены).

Полагаю, я мог бы написать свой собственный, но я на собственном горьком опыте убедился, что изобретение колеса для основных алгоритмов может быть опасным для здоровья: /


person Jason S    schedule 15.06.2009    source источник


Ответы (10)


Map должен быть O (1) в среднем случае большинства реализаций. Поскольку карта сохраняет порядок вставки, добавление небольшого количества кода вокруг нее даст вам LRU, и для большинства случаев это должно быть достаточно быстрым.

Мне нужен был простой LRU-кеш для небольшого количества дорогостоящих операций (1 секунда). Мне было легче скопировать небольшой код вместо того, чтобы вводить что-то внешнее, но, поскольку я не нашел этого, я написал это:

class LRU {
    constructor(max = 10) {
        this.max = max;
        this.cache = new Map();
    }

    get(key) {
        let item = this.cache.get(key);
        if (item) {
            // refresh key
            this.cache.delete(key);
            this.cache.set(key, item);
        }
        return item;
    }

    set(key, val) {
        // refresh key
        if (this.cache.has(key)) this.cache.delete(key);
        // evict oldest
        else if (this.cache.size == this.max) this.cache.delete(this.first());
        this.cache.set(key, val);
    }

    first() {
        return this.cache.keys().next().value;
    }
}

Использование:

> let cache = new LRU(3)
> [1, 2, 3, 4, 5].forEach(v => cache.set(v, 'v:'+v))
> cache.get(2)
undefined
> cache.get(3)
"v:3"
> cache.set(6, 6)
> cache.get(4)
undefined
> cache.get(3)
"v:3"
person odinho - Velmont    schedule 26.09.2017
comment
как у вас работает this.first ()? Карта не предоставляет first() - person Taran Goel; 31.05.2020
comment
@TaranGoel .first() реализован прямо в коде. Просто посмотрите ниже set(). - person odinho - Velmont; 01.06.2020

Этот:

https://github.com/monsur/jscache

кажется, вам подходит, хотя setItem (т.е. положить) - это O (N) в худшем случае, это происходит, если кеш заполняется при вставке. В этом случае выполняется поиск в кэше, чтобы удалить элементы с истекшим сроком действия или элементы, которые использовались недавно. getItem равно O (1), и истечение срока обрабатывается в операции getItem (т.е. если срок действия извлекаемого элемента истек, удаляет его и возвращает ноль).

Код достаточно компактен, чтобы его было легко понять.

P.S. Может быть полезно добавить в конструктор возможность указать fillFactor, который имеет фиксированное значение 0,75 (это означает, что при очистке кеша его размер уменьшается по крайней мере до 3/4 от максимального размера)

person LucaM    schedule 15.06.2009
comment
спасибо, я наткнулся на это. Казалось, что у него слишком много наворотов для моего приложения (не говоря уже о фразе ASP.NET, которая в моем сознании является огромным красным флажком), но, возможно, мне стоит взглянуть на нее еще раз. - person Jason S; 15.06.2009
comment
+1 Реализация не имеет ничего общего с ASP.NET Думаю, стоит посмотреть - person Sam Saffron; 14.07.2009

Об этом стоит упомянуть: https://github.com/rsms/js-lru

Базовый набор функций - O (1), а код сильно комментируется (с ASCII-артом тоже!)

person Corin    schedule 03.11.2014

Реализация monsur.com - O (n) при вставке только потому, что у нее есть элементы, срок действия которых истекает в реальном времени. Это не простой LRU. Если вы заботитесь только о последних использованных элементах без учета реального времени, это можно сделать в O (1). Очередь, реализованная как двусвязный список, имеет значение O (1) для вставки или удаления с конца, и это все, что вам нужно для кеширования. Что касается поиска, то хеш-карта, которую javascript упрощает до некоторой степени, должна подходить для поиска почти O (1) (при условии, что движок javascript использует хорошую хэш-карту, это, конечно, зависит от реализации). Итак, у вас есть связанный список элементов с хеш-картой, указывающей на элементы. При необходимости манипулируйте концами связанного списка, чтобы поместить новые элементы и запрошенные элементы на один конец и удалить старые элементы с другого.

person Electron    schedule 27.07.2010
comment
связанный список должен иметь возможность удалять (но не вставлять) элементы из середины, если элементы удаляются из кэша LRU и повторно вставляются. Это самая сложная часть, ваш ответ, кажется, затушевывает это. - person Jason S; 28.07.2010
comment
Кроме того, удаление из середины двусвязного списка - это O (n), что вам нужно будет сделать, чтобы поддерживать инвариант LRU при доступе. - person Eloff; 09.03.2011
comment
@Eloff, есть дополнительная хеш-карта для перехода к любому элементу в любом месте списка с O (1). Но вы и Джейсон С. правы в том, что манипулирования концами недостаточно, любой элемент в любой позиции в списке может быть следующим, который должен вернуться на переднюю позицию, поэтому при вставке на одном конце удаление может быть из любого положения. Тем не менее, благодаря хэш-карте, которую можно сделать независимо от длины списка. - person Mörre; 12.05.2017

Эта библиотека runtime-memcache реализует lru и несколько других схем кэширования в javascript.

Он использует модифицированный двусвязный список для достижения O (1) для get, set и remove. Вы можете проверить довольно простую реализацию.

person Tushar Sharma    schedule 23.01.2020
comment
интересно ... не могли бы вы прокомментировать любое тестирование / экспертную оценку, которая поможет кому-то оправдать использование этой библиотеки? (Мой вопрос был задан в 2009 году, и я понятия не имел, что я делал в то время, но, возможно, ваш ответ может помочь кому-то другому) - person Jason S; 28.05.2020

Это не кеш LRU, но у меня есть мой реализация собственной связанной карты. Поскольку он использует объекты JS в качестве хранилища, он будет иметь аналогичные характеристики производительности (объекты-оболочки и хеш-функция ухудшают производительность).

В настоящее время документация в основном не -existant, но есть связанный SO-ответ.

Метод each() передаст текущий ключ, текущее значение и логическое значение, указывающее, есть ли еще элементы в качестве аргументов функции обратного вызова.

В качестве альтернативы цикл можно выполнить вручную с помощью

for(var i = map.size; i--; map.next()) {
    var currentKey = map.key();
    var currentValue = map.value();
}
person Christoph    schedule 15.06.2009

Я понимаю, что это старый вопрос, но я добавляю ссылку для использования в будущем. Ознакомьтесь с https://github.com/monmohan/dsjslib. В нем есть реализация LRU Cache в дополнение к некоторым другим структурам данных. Такие кеши (и этот тоже) поддерживают двусвязный список записей кеша в порядке LRU, то есть записи перемещаются в начало по мере доступа к ним и удаляются из хвоста, когда они возвращаются (скажем, по истечении срока действия или из-за того, что был достигнут предел размера). Его O (1), поскольку он включает только постоянное количество манипуляций с указателем.

person factotum    schedule 05.09.2013

(Это просто длинный комментарий)

Вот кеш часов (который является приближением LRU), который работает только со строками / целыми числами в качестве ключей и имеет только метод get:

  • O (1) попадание в кеш, нет движения узла (дружественный сборщик мусора)

  • O (1) промах в кэше, асинхронный

  • количество асинхронных средств доступа (или асинхронных промахов кеша) должно быть равно или меньше размера кеша, в противном случае возникает временная тупиковая блокировка

lrucache.js:

'use strict';
/* 
cacheSize: number of elements in cache, constant, must be greater than or equal to number of asynchronous accessors / cache misses
callbackBackingStoreLoad: user-given cache-miss function to load data from datastore
elementLifeTimeMs: maximum miliseconds before an element is invalidated, only invalidated at next get() call with its key
*/

let Lru = function(cacheSize,callbackBackingStoreLoad,elementLifeTimeMs=1000){
    const me = this;
    
    const maxWait = elementLifeTimeMs;
    const size = parseInt(cacheSize,10);
    const mapping = {};
    const mappingInFlightMiss = {};
    const bufData = new Array(size);
    const bufVisited = new Uint8Array(size);
    const bufKey = new Array(size);
    const bufTime = new Float64Array(size);
    const bufLocked = new Uint8Array(size);
    for(let i=0;i<size;i++)
    {
        let rnd = Math.random();
        mapping[rnd] = i;
        
        bufData[i]="";
        bufVisited[i]=0;
        bufKey[i]=rnd;
        bufTime[i]=0;
        bufLocked[i]=0;
    }
    let ctr = 0;
    let ctrEvict = parseInt(cacheSize/2,10);
    const loadData = callbackBackingStoreLoad;
    let inFlightMissCtr = 0;
    this.get = function(keyPrm,callbackPrm){
        const key = keyPrm;
        const callback = callbackPrm;
        
        // stop dead-lock when many async get calls are made
        if(inFlightMissCtr>=size)
                {
                    setTimeout(function(){
                me.get(key,function(newData){
                    callback(newData);
                });
            },0);
                    return;
            }
        
        // delay the request towards end of the cache-miss completion
        if(key in mappingInFlightMiss)
        {

            setTimeout(function(){
                me.get(key,function(newData){
                    callback(newData);
                });
            },0);
            return;
        }

        if(key in mapping)
        {
            let slot = mapping[key];
            // RAM speed data
            if((Date.now() - bufTime[slot]) > maxWait)
            {
                
                if(bufLocked[slot])
                {                                       
                    setTimeout(function(){
                        me.get(key,function(newData){
                            callback(newData);
                        });
                    },0);
                    
                }
                else
                {
                    delete mapping[key];
                    
                    me.get(key,function(newData){
                        callback(newData);
                    });
                    
                }
                
            }
            else
            {
                bufVisited[slot]=1;
                bufTime[slot] = Date.now();
                callback(bufData[slot]);
            }
        }
        else
        {
            // datastore loading + cache eviction
            let ctrFound = -1;
            while(ctrFound===-1)
            {
                // give slot a second chance before eviction
                if(!bufLocked[ctr] && bufVisited[ctr])
                {
                    bufVisited[ctr]=0;
                }
                ctr++;
                if(ctr >= size)
                {
                    ctr=0;
                }

                // eviction conditions
                if(!bufLocked[ctrEvict] && !bufVisited[ctrEvict])
                {
                    // evict
                    bufLocked[ctrEvict] = 1;
                    inFlightMissCtr++;
                    ctrFound = ctrEvict;
                }

                ctrEvict++;
                if(ctrEvict >= size)
                {
                    ctrEvict=0;
                }
            }
            
            mappingInFlightMiss[key]=1;
            let f = function(res){
                delete mapping[bufKey[ctrFound]];

                bufData[ctrFound]=res;
                bufVisited[ctrFound]=0;
                bufKey[ctrFound]=key;
                bufTime[ctrFound]=Date.now();
                bufLocked[ctrFound]=0;

                mapping[key] = ctrFound;
                callback(bufData[ctrFound]);
                inFlightMissCtr--;
                delete mappingInFlightMiss[key];        
            };
            loadData(key,f);

        }
    };
};

exports.Lru = Lru;

Аннулирование на основе времени происходит только во время операции cache.get (), поэтому он не использует обработчик событий для каких-либо элементов.

Пример использования:

let Lru = require("./lrucache.js").Lru;
let num_cache_elements = 1000;
let element_life_time_miliseconds = 1000;
let cache = new Lru(num_cache_elements, async function(key,callback){
  // datastore access for filling the missing cache element when user access key
  callback(some_time_taking_io_work_or_heavy_computation(key)); 
}, element_life_time_miliseconds);


cache.get("some_key_string",function(data){
    // data comes from datastore or RAM depending on its lifetime left or the key acceess pattern
    // do_something_with(data);
});

Пример кеширования файлов:

let Lru = require("./lrucache.js").Lru;
let fs = require("fs");
let path = require("path");

let fileCache = new Lru(500, async function(key,callback){
  // cache-miss data-load algorithm
    fs.readFile(path.join(__dirname,key),function(err,data){
      if(err) {                                 
        callback({stat:404, data:JSON.stringify(err)});
      }
      else
      {                             
        callback({stat:200, data:data});
      }                                                     
    });
},1000 /* cache element lifetime */);

// test with a file
// cache-miss
let t1 = Date.now();
fileCache.get("./test.js",function(dat){   
  console.log("Cache-miss time:");
  console.log(Date.now()-t1);
  console.log("file data:");
  console.log(dat.data.length+" bytes");
  
  // cache-hit
  t1 = Date.now();
  fileCache.get("./test.js",function(dat){
    console.log("Cache-hit time:");
    console.log(Date.now()-t1);
    console.log("file data:");
    console.log(dat.data.length+" bytes");

  });     
});

Тест асинхронного пропускания кэша:

let Lru = require("./lrucache.js").Lru;
let benchSize = 500;
let cacheMiss= 0;
let cacheAccess= 0;
let cache = new Lru(900, async function(key,callback){
    // cache-miss data-load algorithm
    setTimeout(function(){
        cacheMiss++
        callback(key+" processed");
    },1000);
},1000 /* cache element lifetime */);
    
let ctrBench = 0;

function bench()
{
    let ctr = 0;
    let t1 = Date.now();
    for(let i=0;i<benchSize;i++)
    {
    let key = parseInt(Math.random()*1000,10);
        cache.get(key,function(data){ 
            cacheAccess++;
            if(key.toString()+" processed" !== data)
            {
                console.log("error: wrong key-data mapping.");
                
            }
            if(++ctr === benchSize)
            {
                console.log("benchmark: "+(Date.now()-t1)+" miliseconds");
            console.log("cache hit: "+(cacheAccess-cacheMiss));
            console.log("cache miss: "+(cacheMiss));
            if(ctrBench<20)
            {
                ctrBench++
                bench();
            }
            }
        });
    }
}

bench();

вывод:

benchmark: 1034 miliseconds
cache hit: 125
cache miss: 375
benchmark: 1015 miliseconds
cache hit: 374
cache miss: 626
benchmark: 1017 miliseconds
cache hit: 571
cache miss: 929
benchmark: 1013 miliseconds
cache hit: 750
cache miss: 1250
benchmark: 1016 miliseconds
cache hit: 950
cache miss: 1550
benchmark: 1017 miliseconds
cache hit: 1132
cache miss: 1868
benchmark: 1013 miliseconds
cache hit: 1324
cache miss: 2176
benchmark: 1014 miliseconds
cache hit: 1485
cache miss: 2515
benchmark: 1016 miliseconds
cache hit: 1675
cache miss: 2825
benchmark: 1016 miliseconds
cache hit: 1872
cache miss: 3128
benchmark: 1012 miliseconds
cache hit: 2053
cache miss: 3447
benchmark: 1015 miliseconds
cache hit: 2224
cache miss: 3776
benchmark: 1015 miliseconds
cache hit: 2405
cache miss: 4095
benchmark: 1016 miliseconds
cache hit: 2580
cache miss: 4420
benchmark: 1015 miliseconds
cache hit: 2760
cache miss: 4740
benchmark: 1013 miliseconds
cache hit: 2930
cache miss: 5070
benchmark: 1047 miliseconds
cache hit: 3105
cache miss: 5395
benchmark: 1015 miliseconds
cache hit: 3280
cache miss: 5720
benchmark: 1012 miliseconds
cache hit: 3449
cache miss: 6051
benchmark: 1015 miliseconds
cache hit: 3636
cache miss: 6364
benchmark: 1016 miliseconds
cache hit: 3818
cache miss: 6682

Срок службы 1000 мс здесь снижает коэффициент попадания в кеш. В противном случае ожидается 90% -ный коэффициент попадания.

person huseyin tugrul buyukisik    schedule 05.04.2021

Поскольку нам нужны операции чтения, записи, обновления и удаления в O (1), мы используем две структуры данных.

  1. Объект (или карта) в JavaScript обеспечивает поиск за O (1).
  2. A Doubly LinkedList(Custom data structure we create) makes below functionalities in O(1)
    • change position of the most used element to the top
    • удалить наименее используемый элемент из кеша при достижении лимита кеша.

Ниже приводится настраиваемая реализация кэша Дважды связанных списков и Наименее недавно использованных с четким объяснением.

https://medium.com/dsinjs/implementing-lru-cache-in-javascript-94ba6755cda9

person Uday Vunnam    schedule 24.05.2019
comment
Не ссылайтесь на внешний ресурс, вставьте свою реализацию в ответ. - person odinho - Velmont; 29.05.2020
comment
Если я не ошибаюсь, эта реализация потерпит неудачу при многократном вводе / записи в один и тот же ключ - каждый элемент в кеше будет для одного и того же ключа, что НЕ должно работать с LRU - person Codebling; 15.02.2021

Внешний пакет / библиотека не требуется, мы можем написать наш собственный код для реализации LRU в javascript, пожалуйста, обратитесь к https://dev.to/udayvunnam/implementing-lru-cache-in-javascript-3c8g, чтобы узнать подробности.

person Aditya Anand    schedule 19.05.2020
comment
Пожалуйста, объясните свой ответ дополнительной информацией, а не только ссылкой на внешний источник. - person cnnr; 19.05.2020
comment
Это тот же ответ, что и другой ответ. Другой ответ был опубликован годом ранее первоначальным автором. - person Codebling; 15.02.2021