Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@isaacs
Last active December 8, 2022 19:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save isaacs/4d700a8b80a3f898bc25c1f34aa84dda to your computer and use it in GitHub Desktop.
Save isaacs/4d700a8b80a3f898bc25c1f34aa84dda to your computer and use it in GitHub Desktop.
diff --git a/index.js b/index.js
index fa53c12..620cf57 100644
--- a/index.js
+++ b/index.js
@@ -849,6 +849,114 @@ class LRUCache {
}
}
+ getSet(
+ k,
+ v,
+ {
+ // get options
+ allowStale = this.allowStale,
+ updateAgeOnGet = this.updateAgeOnGet,
+ noDeleteOnStaleGet = this.noDeleteOnStaleGet,
+ // set options
+ ttl = this.ttl,
+ start,
+ noDisposeOnSet = this.noDisposeOnSet,
+ size = 0,
+ sizeCalculation = this.sizeCalculation,
+ noUpdateTTL = this.noUpdateTTL,
+ } = {}
+ ) {
+ let index = this.size === 0 ? undefined : this.keyMap.get(k)
+ if (index !== undefined) {
+ const value = this.valList[index]
+ const fetching = this.isBackgroundFetch(value)
+ if (this.isStale(index)) {
+ // delete only if not an in-flight background fetch
+ if (!fetching) {
+ if (allowStale) {
+ // only need to delete the stale entry if we're returning it
+ // otherwise we'll overwrite it soon anyway.
+ if (!noDeleteOnStaleGet) {
+ this.delete(k)
+ }
+ return value
+ }
+ } else {
+ if (
+ allowStale &&
+ value.__staleWhileFetching !== undefined
+ ) {
+ return value.__staleWhileRefetching
+ }
+ }
+ } else {
+ // if we're currently fetching it, we don't actually have it yet
+ // it's not stale, which means this isn't a staleWhileRefetching,
+ // so we just return undefined
+ if (!fetching) {
+ this.moveToTail(index)
+ if (updateAgeOnGet) {
+ this.updateItemAge(index)
+ }
+ return value
+ }
+ }
+ }
+
+ // ok, either we don't have it, or it's stale and we don't allowStale
+ // set and return the new value. this is the same as set(), but
+ // takes advantage of the fact that we may already know the index.
+ size = this.requireSize(k, v, size, sizeCalculation)
+ if (this.maxEntrySize && size > this.maxEntrySize) {
+ this.delete(k)
+ return v
+ }
+ if (index === undefined) {
+ // addition
+ index = this.newIndex()
+ this.keyList[index] = k
+ this.valList[index] = v
+ this.keyMap.set(k, index)
+ this.next[this.tail] = index
+ this.prev[index] = this.tail
+ this.tail = index
+ this.size++
+ this.addItemSize(index, size)
+ noUpdateTTL = false
+ } else {
+ // update
+ const oldVal = this.valList[index]
+ if (v !== oldVal) {
+ if (this.isBackgroundFetch(oldVal)) {
+ oldVal.__abortController.abort()
+ } else {
+ if (!noDisposeOnSet) {
+ this.dispose(oldVal, k, 'set')
+ if (this.disposeAfter) {
+ this.disposed.push([oldVal, k, 'set'])
+ }
+ }
+ }
+ this.removeItemSize(index)
+ this.valList[index] = v
+ this.addItemSize(index, size)
+ }
+ this.moveToTail(index)
+ }
+ if (ttl !== 0 && this.ttl === 0 && !this.ttls) {
+ this.initializeTTLTracking()
+ }
+ if (!noUpdateTTL) {
+ this.setItemTTL(index, ttl, start)
+ }
+ if (this.disposeAfter) {
+ while (this.disposed.length) {
+ this.disposeAfter(...this.disposed.shift())
+ }
+ }
+ return v
+ }
+
get(
k,
{
const LRUCache = require('../')
const bench = require('bench')
const N = 1000
const keys = new Array(N)
for (let i = 0; i < N; i++) {
keys[i] = Math.random()
}
const autoGetSetCache = new LRUCache({ max: N * 10 })
// fill up first
for (let i = 0; i < N; i++) {
autoGetSetCache.set(keys[i], i)
}
const autoGetSet = () => {
// do 2000 sets, alternating between known and unknown values
for (let i = 0; i < N; i++) {
autoGetSetCache.getSet(keys[i], i + 1)
autoGetSetCache.getSet(Math.random(), i)
}
}
const manualHasGetSetCache = new LRUCache({ max: N * 10 })
for (let i = 0; i < N; i++) {
manualHasGetSetCache.set(keys[i], i)
}
const manualHasGetSet = () => {
for (let i = 0; i < N; i++) {
const k1 = keys[i]
if (manualHasGetSetCache.has(k1)) {
manualHasGetSetCache.get(k1)
} else {
manualHasGetSetCache.set(k1, i + 1)
}
const k2 = Math.random()
if (manualHasGetSetCache.has(k2)) {
manualHasGetSetCache.get(k2)
} else {
manualHasGetSetCache.set(k2, Math.random())
}
}
}
const manualGetSetCache = new LRUCache({ max: N * 10 })
for (let i = 0; i < N; i++) {
manualGetSetCache.set(keys[i], i)
}
const manualGetSet = () => {
for (let i = 0; i < N; i++) {
const k1 = keys[i]
if (manualGetSetCache.get(k1) === undefined) {
manualGetSetCache.set(k1, i + 1)
}
const k2 = Math.random()
if (manualGetSetCache.get(k2) === undefined) {
manualGetSetCache.set(k2, Math.random())
}
}
}
// do a card deck style cut-and-merge shuffle,
// but not a perfect shuffle, muck it up it a bit.
const shuffle = () => {
const high = keys.slice(0, N/2)
const low = keys.slice(N/2)
keys.length = 0
for (let i = 0; i < N/2; i++) {
const rif = Math.random() < 0.5
keys.push(rif ? high[i] : low[i])
keys.push(rif ? low[i] : high[i])
}
}
// shuffle 7 times
for (let i = 0; i < 7; i++) {
shuffle()
}
exports.compare = {
autoGetSet,
manualHasGetSet,
manualGetSet,
}
bench.runMain()
/*
benchmarking /Users/isaacs/dev/isaacs/lru-cache/x/index.js
Please be patient.
{
node: '19.1.0',
v8: '10.7.193.20-node.19',
uv: '1.44.2',
zlib: '1.2.11',
brotli: '1.0.9',
ares: '1.18.1',
modules: '111',
nghttp2: '1.47.0',
napi: '8',
llhttp: '8.1.0',
openssl: '3.0.7+quic',
cldr: '42.0',
icu: '72.1',
tz: '2022e',
unicode: '15.0',
ngtcp2: '0.8.1',
nghttp3: '0.7.0'
}
Scores: (bigger is better)
autoGetSet
Raw:
> 5.514705882352941
> 5.45950864422202
> 5.449591280653951
> 5.489478499542543
Average (mean) 5.478321076692864
manualGetSet
Raw:
> 5.016722408026756
> 5.008347245409015
> 4.930966469428008
> 5
Average (mean) 4.989009030715945
manualHasGetSet
Raw:
> 4.725897920604915
> 4.734848484848484
> 4.752851711026616
> 4.7664442326024785
Average (mean) 4.745010587270623
Winner: autoGetSet
Compared with next highest (manualGetSet), it's:
8.93% faster
1.1 times as fast
0.04 order(s) of magnitude faster
A LITTLE FASTER
Compared with the slowest (manualHasGetSet), it's:
13.39% faster
1.15 times as fast
0.06 order(s) of magnitude faster
*/
{
"dependencies": {
"bench": "^0.3.6",
"lru-cache": "file:.."
}
}
@isaacs
Copy link
Author

isaacs commented Dec 8, 2022

These numbers are in kHz (ops / ms), and each "operation" is actually 2000 get/set operations (1000 with existing keys, 1000 with new keys).

So, best case, this is speeding it up from about 9,500,000 operations per second to about 10,000,000.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment