├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── bench.js ├── index.d.ts ├── index.js ├── package.json └── test └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | *.log 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | - '0.12' 5 | - '4' 6 | - '5' 7 | - '6' 8 | - '7' -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 'Dominic Tarr' 2 | 3 | Permission is hereby granted, free of charge, 4 | to any person obtaining a copy of this software and 5 | associated documentation files (the "Software"), to 6 | deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, 8 | merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom 10 | the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR 20 | ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hashlru 2 | 3 | Simpler, faster LRU cache algorithm 4 | 5 | A Least Recently Used cache is used to speedup requests to a key-value oriented resource, 6 | while making a bounded memory commitment. 7 | 8 | I've recently [benchmarked the various lru implementations available on npm](https://github.com/dominictarr/bench-lru) 9 | and found wildly varing performance. There where some that performed well overall, 10 | and others that performed extremely well in some cases, but poorly in others, due to 11 | compromises made to maintain correctness. 12 | 13 | After writing the benchmark, of course I had to try my hand at my own LRU implementation. 14 | I soon found a few things, LRUs are quite difficult to implement, first of all contain a linked 15 | list. LRUs use a linked list to maintain the order 16 | that keys have been accessed, so that when the cache fills, the old values 17 | (which presumably are the least likely to be needed again) can be removed from the cache. 18 | Linked Lists are not easy to implement correctly! 19 | 20 | Then I discovered why some of the fast algorithms where so slow - they used `delete cache[key]` 21 | which is much slower than `cache[key] = value`, much much slower. 22 | 23 | So, why looking for a way to avoid `delete` I had an idea - have two cache objects, 24 | and when one fills - create a new one and start putting items in that, and then it's sufficiently 25 | full, throw it away. It avoids delete, at at max, only commits us to only N values and between N and 2N keys. 26 | 27 | Then I realized with this pattern, you _don't actually need_ the linked list anymore! 28 | This makes a N-2N least recently used cache very very simple. This both has performance benefits, 29 | and it's also very easy to verify it's correctness. 30 | 31 | This algorithm does not give you an ordered list of the N most recently used items, 32 | but you do not really need that! The property of dropping the least recent items is still preserved. 33 | 34 | see a [benchmark](https://github.com/dominictarr/bench-lru) of this against 35 | the other LRU implementations on npm. 36 | 37 | ## example 38 | 39 | ``` js 40 | var HLRU = require('hashlru') 41 | var lru = HLRU(100) 42 | lru.set(key, value) 43 | lru.get(key) 44 | ``` 45 | 46 | ## algorithm 47 | 48 | create two caches - `old_cache` and `new_cache`, and a counter, `size`. 49 | 50 | When an `key, value` pair is added, if `key` is already in `new_cache` update the value, 51 | not currently in `new_cache`, set `new_cache[key] = value`. 52 | If the key was _not_ already in `new_cache` then `size` is incremented. 53 | If `size > max`, move the `old_cache = new_cache`, reset `size = 0`, and initialize a new `new_cache={}` 54 | 55 | To get a `key`, check if `new_cache` contains key, and if so, return it. 56 | If not, check if it is in `old_cache` and if so, move that value to `new_cache`, and increment `size`. 57 | If `size > max`, move the `old_cache = new_cache`, reset `size = 0`, and initialize a new `new_cache={}` 58 | 59 | ## complexity 60 | 61 | Writes are O(1) on average, like a hash table. 62 | 63 | When implemented in a garbage collected language, the old cache is thrown away when the new cache is 64 | full. To better manage memory usage, it could also be implemented as two fixes sized hash tables. 65 | In this case, instead of discarding the old cache, it would be zeroed. This means at most every N 66 | writes when the caches are rotated, that write will require N operations (to clear the old cache) 67 | 68 | This still averages out to O(1) but it does cost O(N) but only every N writes (except for updates) 69 | so N/N is still 1. 70 | 71 | ## HashLRU(max) => lru 72 | 73 | initialize a lru object. 74 | 75 | ### lru.get(key) => value | undefined 76 | 77 | The key may be strings, numbers or objects (by reference). 78 | 79 | Returns the value in the cache, or `undefined` if the value is not in the cache. 80 | 81 | ### lru.set(key, value) 82 | 83 | The key may be strings, numbers or objects (by reference). 84 | 85 | update the value for key. 86 | 87 | ### lru.has(key) => boolean 88 | 89 | Checks if the `key` is in the cache. 90 | 91 | ### lru.remove(key) 92 | 93 | Removes the `key` from the cache. 94 | 95 | ### lru.clear() 96 | 97 | Empties the entire cache. 98 | 99 | ## License 100 | 101 | MIT 102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /bench.js: -------------------------------------------------------------------------------- 1 | var Stats = require('statistics/mutate') 2 | var LRU = require('./') 3 | 4 | //simple benchmarks, and measure standard deviation 5 | 6 | function run (N, op, init) { 7 | var stats = null, value 8 | for(var j = 0; j < 100; j++) { 9 | if(init) value = init(j) 10 | var start = Date.now() 11 | for(var i = 0; i < N; i++) op(value, i) 12 | stats = Stats(stats, N/((Date.now() - start))) 13 | } 14 | return stats 15 | } 16 | 17 | //set 1000 random items, then read 10000 items. 18 | //since they are random, there will be misses as well as hits 19 | console.log('GET', run(100000, function (lru, n) { 20 | lru.get(~~(Math.random()*1000)) 21 | // lru.set(n, Math.random()) 22 | }, function () { 23 | var lru = LRU(1000) 24 | for(var i = 0; i ++ ; i < 1000) 25 | lru.set(~~(Math.random()*1000), Math.random()) 26 | return lru 27 | })) 28 | 29 | //set 100000 random values into LRU for 1000 values. 30 | //this means 99/100 should be evictions 31 | console.log('SET', run(100000, function (lru, n) { 32 | lru.set(~~(Math.random()*100000), Math.random()) 33 | }, function () { 34 | return LRU(1000) 35 | })) 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export default function HLRU(max: number): { 2 | has: (key: string | number) => boolean; 3 | remove: (key: string | number) => void; 4 | get: (key: string | number) => any; 5 | set: (key: string | number, value: any) => void; 6 | clear: () => void; 7 | }; 8 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = function (max) { 2 | 3 | if (!max) throw Error('hashlru must have a max value, of type number, greater than 0') 4 | 5 | var size = 0, cache = Object.create(null), _cache = Object.create(null) 6 | 7 | function update (key, value) { 8 | cache[key] = value 9 | size ++ 10 | if(size >= max) { 11 | size = 0 12 | _cache = cache 13 | cache = Object.create(null) 14 | } 15 | } 16 | 17 | return { 18 | has: function (key) { 19 | return cache[key] !== undefined || _cache[key] !== undefined 20 | }, 21 | remove: function (key) { 22 | if(cache[key] !== undefined) 23 | cache[key] = undefined 24 | if(_cache[key] !== undefined) 25 | _cache[key] = undefined 26 | }, 27 | get: function (key) { 28 | var v = cache[key] 29 | if(v !== undefined) return v 30 | if((v = _cache[key]) !== undefined) { 31 | update(key, v) 32 | return v 33 | } 34 | }, 35 | set: function (key, value) { 36 | if(cache[key] !== undefined) cache[key] = value 37 | else update(key, value) 38 | }, 39 | clear: function () { 40 | cache = Object.create(null) 41 | _cache = Object.create(null) 42 | } 43 | } 44 | } 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hashlru", 3 | "description": "simpler faster substitute for LRU", 4 | "version": "2.3.0", 5 | "homepage": "https://github.com/dominictarr/hashlru", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/dominictarr/hashlru.git" 9 | }, 10 | "dependencies": {}, 11 | "devDependencies": { 12 | "istanbul": "^0.4.5" 13 | }, 14 | "types": "index.d.ts", 15 | "scripts": { 16 | "test": "set -e; for t in test/*.js; do node $t; done", 17 | "cov": "istanbul cover test/*.js" 18 | }, 19 | "author": "'Dominic Tarr' (dominictarr.com)", 20 | "license": "MIT" 21 | } 22 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var HLRU = require('../') 3 | var lru = HLRU(2) 4 | 5 | // set-get: 6 | lru.set('test', 'test') 7 | 8 | assert.equal(lru.get('test'), 'test') 9 | 10 | // has: 11 | assert.equal(lru.has('test'), true) 12 | assert.equal(lru.has('blah'), false) 13 | 14 | // update: 15 | lru.set('test', 'test2') 16 | 17 | assert.equal(lru.get('test'), 'test2') 18 | 19 | // cache cycle: 20 | lru.set('test2', 'test') 21 | 22 | assert.equal(lru.get('test2'), 'test') 23 | 24 | // get previous after cache cycle: 25 | assert.equal(lru.get('test'), 'test2') 26 | 27 | // update new cache: 28 | lru.set('test2', 'test2') 29 | 30 | assert.equal(lru.get('test2'), 'test2') 31 | 32 | // object purity: 33 | assert.equal(lru.get('constructor'), undefined) 34 | 35 | // max validation: 36 | assert.throws(HLRU) 37 | 38 | // remove: 39 | assert.equal(lru.has('test2'), true) 40 | lru.remove('test2') 41 | assert.equal(lru.has('test2'), false) 42 | 43 | // clear 44 | assert.equal(lru.has('test'), true) 45 | lru.clear() 46 | assert.equal(lru.has('test'), false) 47 | --------------------------------------------------------------------------------