Javascript中的LRU缓存实现

时间:2009-06-15 14:42:18

标签: javascript algorithm caching lru

Java有LinkedHashMap gets you 99% there to an LRU cache

是否存在LRU缓存的Javascript实现,最好是来自信誉良好的源,即:

  1. 可以理解的
  2. 高效(摊销O(1)获取/存放/删除)
  3. ?我一直在网上搜索但找不到一个;我以为我在Ajax Design Patterns上找到了一个,但它掩盖了sendToTail()方法并具有O(n)性能(可能是因为队列和关联数组被拆分)。

    我想我可以写自己的,但我已经知道重新发明核心算法的轮子会对一个人的健康造成危害:/

10 个答案:

答案 0 :(得分:8)

此:

https://github.com/monsur/jscache

似乎适合你的情况虽然setItem(即put)在最坏的情况下是O(N),如果在插入时填充了缓存,则会发生这种情况。在这种情况下,搜索缓存以清除过期的项目或最近最少使用的项目。 getItem为O(1),并且在getItem操作上处理到期(即,如果正在提取的项目已过期,则将其删除并返回null)。

代码非常紧凑,易于理解。

P.S。向构造函数添加指定fillFactor的选项可能很有用,该选项固定为0.75(意味着当清除缓存时,它的大小至少减少到最大大小的3/4)

答案 1 :(得分:6)

这不如您想要的那么高效,但它在简单性和可读性方面弥补了它。在许多情况下,它会很快。

我需要一个简单的LRU缓存来进行少量昂贵的操作(1秒)。我觉得复制粘贴一些小代码而不是介绍一些外部代码感觉更好,但是因为我没有找到它,所以我写了它:

更新:由于我删除了数组,因此现在提高了效率(空间和时间),因为Map保持了插入顺序。

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) {
        if (this.cache.has(key)) // refresh key
            this.cache.delete(key);
        else if (this.cache.size == this.max) // evict oldest
            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"

答案 2 :(得分:5)

值得一提的是: https://github.com/rsms/js-lru

核心功能集是O(1),代码评论很多(也有ASCII艺术!)

答案 3 :(得分:3)

monsur.com实现仅在插入时为O(n),因为它具有实际在实际时间到期的项目。它不是一个简单的LRU。如果您只关心维护最近使用的项目而不考虑现实世界时间,可以在O(1)中完成。实现为双向链表的队列是O(1),用于从末尾插入或删除,这是缓存所需的全部内容。至于查找,一个哈希映射,javascript变得非常简单,应该适合于近O(1)查找(假设javascript引擎使用了一个好的hashmap,当然这依赖于实现)。因此,您有一个包含指向项目的哈希映射的项目的链接列表。根据需要操作链表的末尾,将新项目和请求的项目放在一端,并从另一端删除旧项目。

答案 4 :(得分:1)

此库runtime-memcache在javascript中实现了lru和其他一些缓存方案。

它使用修改后的双链表为getsetremove实现O(1)。您可以签出非常简单的实现。

答案 5 :(得分:0)

它不是LRU缓存,但我有my own linked map implementation。因为它使用JS对象作为存储,所以它具有类似的性能特征(包装器对象和散列函数会导致性能损失)。

目前,文档为basically non-existant,但有一个related SO answer

each()方法将传递当前键,当前值和一个布尔值,指示是否有更多元素作为回调函数的参数。

或者,循环可以通过

手动完成
for(var i = map.size; i--; map.next()) {
    var currentKey = map.key();
    var currentValue = map.value();
}

答案 6 :(得分:0)

我知道这是一个老问题,但为未来的参考添加了一个链接。 查看https://github.com/monmohan/dsjslib。除了一些其他数据结构之外,它还具有LRU Cache实现。这样的高速缓存(以及这个高速缓存)也以LRU顺序维护高速缓存条目的双向链接列表,即当条目被访问时条目移动到头部,并且当它们被回收时(例如通过到期或因为达到大小限制)从尾部移除。它的O(1)因为它只涉及恒定数量的指针操作。

答案 7 :(得分:0)

由于我们需要在O(1)中进行读取,写入,更新和删除操作,因此我们使用两个数据结构。

  1. JavaScript中的对象(或地图)提供O(1)中的检索。
  2. 一个双链表(我们创建的自定义数据结构)在O(1)中具有以下功能
    • 将最常用元素的位置更改为顶部
    • 达到高速缓存限制时从高速缓存中删除最少使用的元素。

下面给出了双向链接列表最近最少使用缓存的自定义实现,

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

答案 8 :(得分:0)

不需要外部程序包/库,我们可以编写自己的代码以用javascript实现LRU,有关详细信息,请访问https://dev.to/udayvunnam/implementing-lru-cache-in-javascript-3c8g网站。

答案 9 :(得分:0)

(这只是一个长评论)

这是一个时钟(它是 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%。