├── .gitignore ├── .travis.yml ├── package.json ├── LICENSE ├── test.js ├── bench.js ├── README.md ├── crc16.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | - '0.12' 5 | - '4' 6 | - '6' 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "array-lru", 3 | "version": "1.1.1", 4 | "description": "A really fast LRU cache for array items (numeric keys)", 5 | "main": "index.js", 6 | "dependencies": {}, 7 | "devDependencies": { 8 | "hashlru": "^1.0.6", 9 | "nanobench": "^1.0.3", 10 | "standard": "^8.6.0", 11 | "tape": "^4.6.3" 12 | }, 13 | "scripts": { 14 | "test": "standard && tape test.js", 15 | "bench": "node bench.js" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/mafintosh/array-lru.git" 20 | }, 21 | "author": "Mathias Buus (@mafintosh)", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/mafintosh/array-lru/issues" 25 | }, 26 | "homepage": "https://github.com/mafintosh/array-lru" 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Mathias Buus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape') 2 | var alru = require('./') 3 | 4 | tape('set and get', function (t) { 5 | var lru = alru(8) 6 | lru.set(42, 'hello') 7 | lru.set(59244, 'world') 8 | t.same(lru.get(43), null) 9 | t.same(lru.get(42), 'hello') 10 | t.same(lru.get(59244), 'world') 11 | t.end() 12 | }) 13 | 14 | tape('overflow', function (t) { 15 | var evicted = [] 16 | var lru = alru(8, { 17 | evict: function (index, value) { 18 | t.same(value, 'hello-' + index) 19 | evicted.push(index) 20 | } 21 | }) 22 | 23 | for (var i = 0; i < 16; i++) { 24 | lru.set(i, 'hello-' + i) 25 | } 26 | 27 | for (var j = 0; j < 16; j++) { 28 | var e = evicted.indexOf(j) > -1 29 | if (e) t.same(lru.get(j), null, 'should be evicted') 30 | else t.same(lru.get(j), 'hello-' + j, 'not evicted') 31 | } 32 | 33 | t.end() 34 | }) 35 | 36 | tape('lru', function (t) { 37 | var lru = alru(8) 38 | 39 | for (var i = 0; i < 16; i++) { 40 | lru.get(0) 41 | lru.get(1) 42 | lru.get(2) 43 | lru.set(i, 'hello-' + i) 44 | } 45 | 46 | t.same(lru.get(0), 'hello-0') 47 | t.same(lru.get(1), 'hello-1') 48 | t.same(lru.get(2), 'hello-2') 49 | 50 | t.end() 51 | }) 52 | -------------------------------------------------------------------------------- /bench.js: -------------------------------------------------------------------------------- 1 | var bench = require('nanobench') 2 | var alru = require('./') 3 | var hlru = require('hashlru') 4 | 5 | bench('insert 750.000 values (hashlru)', function (b) { 6 | var lru = hlru(65536) 7 | 8 | for (var i = 0; i < 750000; i++) { 9 | lru.set('' + i, i) 10 | } 11 | 12 | b.end() 13 | }) 14 | 15 | bench('insert+get 750.000 values (hashlru)', function (b) { 16 | var lru = hlru(65536) 17 | 18 | for (var i = 0; i < 750000; i++) { 19 | var k = '' + i 20 | lru.set(k, i) 21 | lru.get(k) 22 | } 23 | 24 | b.end() 25 | }) 26 | 27 | bench('insert 750.000 values (array-lru)', function (b) { 28 | var lru = alru(65536) 29 | 30 | for (var i = 0; i < 750000; i++) { 31 | lru.set(i, i) 32 | } 33 | 34 | b.end() 35 | }) 36 | 37 | bench('insert+get 750.000 values (array-lru)', function (b) { 38 | var lru = alru(65536) 39 | 40 | for (var i = 0; i < 750000; i++) { 41 | lru.set(i, i) 42 | lru.get(i) 43 | } 44 | 45 | b.end() 46 | }) 47 | 48 | bench('insert 750.000 values (array-lru, 8 collisions)', function (b) { 49 | var lru = alru(65536, {collisions: 8}) 50 | 51 | for (var i = 0; i < 750000; i++) { 52 | lru.set(i, i) 53 | } 54 | 55 | b.end() 56 | }) 57 | 58 | bench('insert+get 750.000 values (array-lru, 8 collisions)', function (b) { 59 | var lru = alru(65536, {collisions: 8}) 60 | 61 | for (var i = 0; i < 750000; i++) { 62 | lru.set(i, i) 63 | lru.get(i) 64 | } 65 | 66 | b.end() 67 | }) 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # array-lru 2 | 3 | A really fast LRU cache for array items (numeric keys) 4 | 5 | ``` 6 | npm install array-lru 7 | ``` 8 | 9 | [![build status](https://travis-ci.org/mafintosh/array-lru.svg?branch=master)](http://travis-ci.org/mafintosh/array-lru) 10 | 11 | Credit to [@dominictarr](https://github.com/dominictarr) for telling me about this neat algorithm for LRUs. 12 | 13 | ## Usage 14 | 15 | ``` js 16 | var alru = require('array-lru') 17 | var lru = alru(512) // create a lru that can contain 512 values 18 | 19 | lru.set(42, {hello: 'world'}) 20 | console.log(lru.get(42)) // {hello: 'world'} 21 | ``` 22 | 23 | It works similar to a normal hash table except when a bucket is full it will 24 | evict the oldest one from the bucket to make room for the new value. 25 | 26 | ## API 27 | 28 | #### `var lru = alru(size, [options])` 29 | 30 | Create a new LRU instance. Options include: 31 | 32 | ``` js 33 | { 34 | collisions: 4, // how many hash collisions before evicting (default 4) 35 | evict: fn, // call this function with (index, value) when someone is evicted 36 | indexedValues: false // set to true if your values has a .index property 37 | } 38 | ``` 39 | 40 | Size should be a multiple of `collisions`. If not, it will be coerced into one. 41 | 42 | ### `var value = lru.get(index)` 43 | 44 | Get a value from the cache. If the index is not found, `null` is returned. 45 | 46 | ### `lru.set(index, value)` 47 | 48 | Insert a new value in the cache. If there is no room in the hash bucket that 49 | `index` maps to, the oldest value in the bucket will be evicted. 50 | 51 | ### Performance 52 | 53 | On my MacBook 12" I can set/get around 7.500.000 values per second, YMMV. 54 | Run the benchmark using `npm run bench` to test it for yourself. 55 | 56 | ## License 57 | 58 | MIT 59 | -------------------------------------------------------------------------------- /crc16.js: -------------------------------------------------------------------------------- 1 | // crc16 impl, optimized for numeric inputs 2 | 3 | var TABLE = [ 4 | 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, 5 | 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, 6 | 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, 7 | 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, 8 | 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, 9 | 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, 10 | 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, 11 | 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, 12 | 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, 13 | 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, 14 | 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, 15 | 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, 16 | 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, 17 | 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, 18 | 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, 19 | 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, 20 | 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, 21 | 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, 22 | 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, 23 | 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, 24 | 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, 25 | 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 26 | 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, 27 | 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, 28 | 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, 29 | 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, 30 | 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, 31 | 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, 32 | 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, 33 | 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, 34 | 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, 35 | 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0 36 | ] 37 | 38 | module.exports = crc16 39 | 40 | function crc16 (n) { 41 | var crc = 0 42 | var r = 0 43 | 44 | for (var i = 0; i < 8; i++) { 45 | r = n & 0xff 46 | n = (n - r) / 256 47 | crc = ((crc << 8) ^ TABLE[((crc >> 8) ^ r) & 0xff]) & 0xffff 48 | } 49 | 50 | return crc 51 | } 52 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var hash = require('./crc16') 2 | 3 | module.exports = LRU 4 | 5 | function LRU (max, opts) { 6 | if (!(this instanceof LRU)) return new LRU(max, opts) 7 | if (!opts) opts = {} 8 | 9 | // how many collisions before evicting (factor of two for fast modulo) 10 | this.collisions = factorOfTwo(opts.collisions || opts.bucketSize || 4) 11 | // buckets should be a factor of two for fast modulo as well 12 | this.buckets = factorOf(max, this.collisions) / this.collisions 13 | 14 | // we use 16bit hashing to bucket index must be <0xffff 15 | while (this.buckets > 65536) { 16 | this.buckets >>= 1 17 | this.collisions <<= 1 18 | } 19 | 20 | this.size = this.buckets * this.collisions 21 | this.wrap = !opts.indexedValues 22 | this.cache = new Array(this.size) 23 | this.hash = this.buckets === 65536 ? hash : maskedHash(this.buckets - 1) 24 | this.evict = opts.evict || null 25 | } 26 | 27 | LRU.prototype.set = function (index, val) { 28 | var pageStart = this.collisions * this.hash(index) 29 | var pageEnd = pageStart + this.collisions 30 | var ptr = pageStart 31 | var page = null 32 | 33 | while (ptr < pageEnd) { 34 | page = this.cache[ptr] 35 | 36 | if (!page) { 37 | // no exiting version, but we have space to store it 38 | page = this.cache[ptr] = this.wrap ? new Node(index, val) : val 39 | move(this.cache, pageStart, ptr, page) 40 | return 41 | } 42 | 43 | if (page.index === index) { 44 | // update existing version and move to head of bucket 45 | if (this.wrap) page.value = val 46 | else this.cache[ptr] = val 47 | move(this.cache, pageStart, ptr, page) 48 | return 49 | } 50 | 51 | ptr++ 52 | } 53 | 54 | // bucket is full, update oldest (last element in bucket) 55 | if (this.wrap) { 56 | if (this.evict) this.evict(page.index, page.value) 57 | page.index = index 58 | page.value = val 59 | } else { 60 | if (this.evict) this.evict(page.index, page) 61 | this.cache[ptr - 1] = val 62 | } 63 | move(this.cache, pageStart, ptr - 1, page) 64 | } 65 | 66 | LRU.prototype.get = function (index) { 67 | var pageStart = this.collisions * this.hash(index) 68 | var pageEnd = pageStart + this.collisions 69 | var ptr = pageStart 70 | 71 | while (ptr < pageEnd) { 72 | var page = this.cache[ptr++] 73 | 74 | if (!page) return null 75 | if (page.index !== index) continue 76 | 77 | // we found it! move to head of bucket and return value 78 | move(this.cache, pageStart, ptr - 1, page) 79 | 80 | return this.wrap ? page.value : page 81 | } 82 | 83 | return null 84 | } 85 | 86 | function move (list, index, itemIndex, item) { 87 | while (itemIndex > index) list[itemIndex] = list[--itemIndex] 88 | list[index] = item 89 | } 90 | 91 | function Node (index, value) { 92 | this.index = index 93 | this.value = value 94 | } 95 | 96 | function factorOf (n, factor) { 97 | n = factorOfTwo(n) 98 | while (n & (factor - 1)) n <<= 1 99 | return n 100 | } 101 | 102 | function factorOfTwo (n) { 103 | if (n && !(n & (n - 1))) return n 104 | var p = 1 105 | while (p < n) p <<= 1 106 | return p 107 | } 108 | 109 | function maskedHash (mask) { 110 | return function (n) { 111 | return hash(n) & mask 112 | } 113 | } 114 | --------------------------------------------------------------------------------