diff --git a/benchmark/cache.ts b/benchmark/cache.ts index 54ae5184..11522c88 100644 --- a/benchmark/cache.ts +++ b/benchmark/cache.ts @@ -34,7 +34,7 @@ describe('Benchmark:', function () { benchmark('DWC new', () => new Cache(10000), done); }); - for (const size of [1e1, 1e2, 1e3, 1e4, 1e5, 1e6]) { + for (const size of [1e2, 1e3, 1e4, 1e5, 1e6]) { it(`Clock set miss ${size.toLocaleString('en')}`, function (done) { const cache = new Clock(size); for (let i = 0; i < Math.ceil(size / 32) * 32; ++i) cache.set(~i, {}); @@ -84,7 +84,7 @@ describe('Benchmark:', function () { }); } - for (const size of [1e1, 1e2, 1e3, 1e4, 1e5, 1e6]) { + for (const size of [1e2, 1e3, 1e4, 1e5, 1e6]) { it(`Clock set hit ${size.toLocaleString('en')}`, function (done) { const cache = new Clock(size); for (let i = 0; i < Math.ceil(size / 32) * 32; ++i) cache.set(i, {}); @@ -128,7 +128,7 @@ describe('Benchmark:', function () { }); } - for (const size of [1e1, 1e2, 1e3, 1e4, 1e5, 1e6]) { + for (const size of [1e2, 1e3, 1e4, 1e5, 1e6]) { it(`Clock get miss ${size.toLocaleString('en')}`, function (done) { const cache = new Clock(size); for (let i = 0; i < Math.ceil(size / 32) * 32; ++i) cache.set(~i, {}); @@ -172,7 +172,7 @@ describe('Benchmark:', function () { }); } - for (const size of [1e1, 1e2, 1e3, 1e4, 1e5, 1e6]) { + for (const size of [1e2, 1e3, 1e4, 1e5, 1e6]) { it(`Clock get hit ${size.toLocaleString('en')}`, function (done) { const cache = new Clock(size); for (let i = 0; i < Math.ceil(size / 32) * 32; ++i) cache.set(i, {}); @@ -216,21 +216,15 @@ describe('Benchmark:', function () { }); } - // 1e7はシミュだけ実行するとISCが単体でもGitHub Actionsの次の環境とエラーで落ちる。 - // ベンチ全体を実行したときはなぜか落ちない。 - // - // Error: Uncaught RangeError: Map maximum size exceeded (dist/index.js:16418) - // - // System: - // OS: Linux 5.15 Ubuntu 20.04.5 LTS (Focal Fossa) - // CPU: (2) x64 Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz - // Memory: 5.88 GB / 6.78 GB - // - for (const size of [1e1, 1e2, 1e3, 1e4, 1e5, 1e6]) { - const bias = (capacity: number, rng: () => number) => () => rng() * capacity * 10 | 0; + // 遅いZipfを速い疑似関数で代用し偏りを再現する。 + // ILRUのリストをインデクスで置き換える高速化手法はZipfのような + // 最も典型的な偏りのアクセスパターンで50%以下の速度に低速化する場合がある。 + for (const size of [1e2, 1e3, 1e4, 1e5, 1e6]) { + const pzipf = (capacity: number, rng: () => number) => () => + Math.floor((rng() * capacity) ** 2 / (5 * capacity / 100 | 0)); it(`Clock simulation ${size.toLocaleString('en')} 10%`, function (done) { const cache = new Clock(size); - const random = bias(Math.ceil(size / 32) * 32, xorshift.random(1)); + const random = pzipf(Math.ceil(size / 32) * 32, xorshift.random(1)); for (let i = 0; i < size * 10; ++i) cache.set(random(), {}); benchmark(`Clock simulation ${size.toLocaleString('en')} 10%`, () => { const key = random(); @@ -240,7 +234,7 @@ describe('Benchmark:', function () { it(`ILRU simulation ${size.toLocaleString('en')} 10%`, function (done) { const cache = new LRUCache({ max: size }); - const random = bias(size, xorshift.random(1)); + const random = pzipf(size, xorshift.random(1)); for (let i = 0; i < size * 10; ++i) cache.set(random(), {}); benchmark(`ILRU simulation ${size.toLocaleString('en')} 10%`, () => { const key = random(); @@ -250,7 +244,7 @@ describe('Benchmark:', function () { it(`LRU simulation ${size.toLocaleString('en')} 10%`, function (done) { const cache = new LRU(size); - const random = bias(size, xorshift.random(1)); + const random = pzipf(size, xorshift.random(1)); for (let i = 0; i < size * 10; ++i) cache.set(random(), {}); benchmark(`LRU simulation ${size.toLocaleString('en')} 10%`, () => { const key = random(); @@ -260,7 +254,7 @@ describe('Benchmark:', function () { it(`TRC-C simulation ${size.toLocaleString('en')} 10%`, function (done) { const cache = new TRCC(size); - const random = bias(size, xorshift.random(1)); + const random = pzipf(size, xorshift.random(1)); for (let i = 0; i < size * 10; ++i) cache.set(random(), {}); benchmark(`TRC-C simulation ${size.toLocaleString('en')} 10%`, () => { const key = random(); @@ -270,7 +264,7 @@ describe('Benchmark:', function () { it(`TRC-L simulation ${size.toLocaleString('en')} 10%`, function (done) { const cache = new TRCL(size); - const random = bias(size, xorshift.random(1)); + const random = pzipf(size, xorshift.random(1)); for (let i = 0; i < size * 10; ++i) cache.set(random(), {}); benchmark(`TRC-L simulation ${size.toLocaleString('en')} 10%`, () => { const key = random(); @@ -280,7 +274,7 @@ describe('Benchmark:', function () { it(`DWC simulation ${size.toLocaleString('en')} 10%`, function (done) { const cache = new Cache(size); - const random = bias(size, xorshift.random(1)); + const random = pzipf(size, xorshift.random(1)); for (let i = 0; i < size * 10; ++i) cache.set(random(), {}); benchmark(`DWC simulation ${size.toLocaleString('en')} 10%`, () => { const key = random(); @@ -289,11 +283,12 @@ describe('Benchmark:', function () { }); } - for (const size of [1e1, 1e2, 1e3, 1e4, 1e5, 1e6]) { - const bias = (capacity: number, rng: () => number) => () => rng() * capacity * 2 | 0; + for (const size of [1e2, 1e3, 1e4, 1e5, 1e6]) { + const pzipf = (capacity: number, rng: () => number) => () => + Math.floor((rng() * capacity) ** 2 / (35 * capacity / 100 | 0)); it(`Clock simulation ${size.toLocaleString('en')} 50%`, function (done) { const cache = new Clock(size); - const random = bias(Math.ceil(size / 32) * 32, xorshift.random(1)); + const random = pzipf(Math.ceil(size / 32) * 32, xorshift.random(1)); for (let i = 0; i < size * 10; ++i) cache.set(random(), {}); benchmark(`Clock simulation ${size.toLocaleString('en')} 50%`, () => { const key = random(); @@ -303,7 +298,7 @@ describe('Benchmark:', function () { it(`ILRU simulation ${size.toLocaleString('en')} 50%`, function (done) { const cache = new LRUCache({ max: size }); - const random = bias(size, xorshift.random(1)); + const random = pzipf(size, xorshift.random(1)); for (let i = 0; i < size * 10; ++i) cache.set(random(), {}); benchmark(`ILRU simulation ${size.toLocaleString('en')} 50%`, () => { const key = random(); @@ -313,7 +308,7 @@ describe('Benchmark:', function () { it(`LRU simulation ${size.toLocaleString('en')} 50%`, function (done) { const cache = new LRU(size); - const random = bias(size, xorshift.random(1)); + const random = pzipf(size, xorshift.random(1)); for (let i = 0; i < size * 10; ++i) cache.set(random(), {}); benchmark(`LRU simulation ${size.toLocaleString('en')} 50%`, () => { const key = random(); @@ -323,7 +318,7 @@ describe('Benchmark:', function () { it(`TRC-C simulation ${size.toLocaleString('en')} 50%`, function (done) { const cache = new TRCC(size); - const random = bias(size, xorshift.random(1)); + const random = pzipf(size, xorshift.random(1)); for (let i = 0; i < size * 10; ++i) cache.set(random(), {}); benchmark(`TRC-C simulation ${size.toLocaleString('en')} 50%`, () => { const key = random(); @@ -333,7 +328,7 @@ describe('Benchmark:', function () { it(`TRC-L simulation ${size.toLocaleString('en')} 50%`, function (done) { const cache = new TRCL(size); - const random = bias(size, xorshift.random(1)); + const random = pzipf(size, xorshift.random(1)); for (let i = 0; i < size * 10; ++i) cache.set(random(), {}); benchmark(`TRC-L simulation ${size.toLocaleString('en')} 50%`, () => { const key = random(); @@ -343,7 +338,7 @@ describe('Benchmark:', function () { it(`DWC simulation ${size.toLocaleString('en')} 50%`, function (done) { const cache = new Cache(size); - const random = bias(size, xorshift.random(1)); + const random = pzipf(size, xorshift.random(1)); for (let i = 0; i < size * 10; ++i) cache.set(random(), {}); benchmark(`DWC simulation ${size.toLocaleString('en')} 50%`, () => { const key = random(); @@ -352,11 +347,12 @@ describe('Benchmark:', function () { }); } - for (const size of [1e1, 1e2, 1e3, 1e4, 1e5, 1e6]) { - const bias = (capacity: number, rng: () => number) => () => rng() * capacity * 1.1 | 0; + for (const size of [1e2, 1e3, 1e4, 1e5, 1e6]) { + const pzipf = (capacity: number, rng: () => number) => () => + Math.floor((rng() * capacity) ** 2 / (85 * capacity / 100 | 0)); it(`Clock simulation ${size.toLocaleString('en')} 90%`, function (done) { const cache = new Clock(size); - const random = bias(Math.ceil(size / 32) * 32, xorshift.random(1)); + const random = pzipf(Math.ceil(size / 32) * 32, xorshift.random(1)); for (let i = 0; i < size * 10; ++i) cache.set(random(), {}); benchmark(`Clock simulation ${size.toLocaleString('en')} 90%`, () => { const key = random(); @@ -366,7 +362,7 @@ describe('Benchmark:', function () { it(`ILRU simulation ${size.toLocaleString('en')} 90%`, function (done) { const cache = new LRUCache({ max: size }); - const random = bias(size, xorshift.random(1)); + const random = pzipf(size, xorshift.random(1)); for (let i = 0; i < size * 10; ++i) cache.set(random(), {}); benchmark(`ILRU simulation ${size.toLocaleString('en')} 90%`, () => { const key = random(); @@ -376,7 +372,7 @@ describe('Benchmark:', function () { it(`LRU simulation ${size.toLocaleString('en')} 90%`, function (done) { const cache = new LRU(size); - const random = bias(size, xorshift.random(1)); + const random = pzipf(size, xorshift.random(1)); for (let i = 0; i < size * 10; ++i) cache.set(random(), {}); benchmark(`LRU simulation ${size.toLocaleString('en')} 90%`, () => { const key = random(); @@ -386,7 +382,7 @@ describe('Benchmark:', function () { it(`TRC-C simulation ${size.toLocaleString('en')} 90%`, function (done) { const cache = new TRCC(size); - const random = bias(size, xorshift.random(1)); + const random = pzipf(size, xorshift.random(1)); for (let i = 0; i < size * 10; ++i) cache.set(random(), {}); benchmark(`TRC-C simulation ${size.toLocaleString('en')} 90%`, () => { const key = random(); @@ -396,7 +392,7 @@ describe('Benchmark:', function () { it(`TRC-L simulation ${size.toLocaleString('en')} 90%`, function (done) { const cache = new TRCL(size); - const random = bias(size, xorshift.random(1)); + const random = pzipf(size, xorshift.random(1)); for (let i = 0; i < size * 10; ++i) cache.set(random(), {}); benchmark(`TRC-L simulation ${size.toLocaleString('en')} 90%`, () => { const key = random(); @@ -406,7 +402,7 @@ describe('Benchmark:', function () { it(`DWC simulation ${size.toLocaleString('en')} 90%`, function (done) { const cache = new Cache(size); - const random = bias(size, xorshift.random(1)); + const random = pzipf(size, xorshift.random(1)); for (let i = 0; i < size * 10; ++i) cache.set(random(), {}); benchmark(`DWC simulation ${size.toLocaleString('en')} 90%`, () => { const key = random(); @@ -415,14 +411,15 @@ describe('Benchmark:', function () { }); } - for (const size of [1e1, 1e2, 1e3, 1e4, 1e5, 1e6]) { - const bias = (capacity: number, rng: () => number) => () => rng() * capacity * 1.1 | 0; + for (const size of [1e2, 1e3, 1e4, 1e5, 1e6]) { + const pzipf = (capacity: number, rng: () => number) => () => + Math.floor((rng() * capacity) ** 2 / (85 * capacity / 100 | 0)); const age = 1000; it(`ILRU simulation ${size.toLocaleString('en')} 90% expire`, captureTimers(function (done) { const cache = new LRUCache({ max: size, ttlAutopurge: true }); - const random = bias(size, xorshift.random(1)); + const random = pzipf(size, xorshift.random(1)); for (let i = 0; i < size * 9; ++i) cache.set(random(), {}); - for (let i = 0; i < size * 1; ++i) cache.set(i, {}, { ttl: age }); + for (let i = 0; i < size * 1; ++i) cache.set(random(), {}, { ttl: age }); benchmark(`ILRU simulation ${size.toLocaleString('en')} 90% expire`, () => { const key = random(); cache.get(key) ?? cache.set(key, {}, { ttl: age }); @@ -431,9 +428,9 @@ describe('Benchmark:', function () { it(`DWC simulation ${size.toLocaleString('en')} 90% expire`, function (done) { const cache = new Cache(size, { eagerExpiration: true }); - const random = bias(size, xorshift.random(1)); + const random = pzipf(size, xorshift.random(1)); for (let i = 0; i < size * 9; ++i) cache.set(random(), {}); - for (let i = 0; i < size * 1; ++i) cache.set(i, {}, { age: age }); + for (let i = 0; i < size * 1; ++i) cache.set(random(), {}, { age: age }); benchmark(`DWC simulation ${size.toLocaleString('en')} 90% expire`, () => { const key = random(); cache.get(key) ?? cache.add(key, {}, { age: age }); diff --git a/src/cache.test.ts b/src/cache.test.ts index 19e9fb87..6c7f3b6f 100644 --- a/src/cache.test.ts +++ b/src/cache.test.ts @@ -406,6 +406,39 @@ describe('Unit: lib/cache', () => { assert(stats.dwc / stats.lru * 100 >>> 0 === 147); }); + it('ratio pzipf 100', function () { + this.timeout(10 * 1e3); + + const capacity = 100; + const lru = new LRU(capacity); + const trc = new TLRU(capacity); + const dwc = new Cache(capacity); + + const trials = capacity * 1000; + const random = xorshift.random(1); + const stats = new Stats(); + const log = { 0: 0 }; + for (let i = 0; i < trials; ++i) { + const key = Math.floor((random() * capacity) ** 2 / (10 * capacity / 100 | 0)); + stats.lru += lru.get(key) ?? +lru.set(key, 1) & 0; + stats.trc += trc.get(key) ?? +trc.set(key, 1) & 0; + stats.dwc += dwc.get(key) ?? +dwc.set(key, 1) & 0; + stats.total += 1; + key in log ? ++log[key] : log[key] = 1; + } + //console.debug(Object.entries(log).sort((a, b) => b[1] - a[1]).map(e => [+e[0], e[1]])); + assert(dwc['LRU'].length + dwc['LFU'].length === dwc['dict'].size); + assert(dwc['dict'].size <= capacity); + console.debug('Cache pzipf 100'); + console.debug('LRU hit ratio', stats.lru * 100 / stats.total); + console.debug('TRC hit ratio', stats.trc * 100 / stats.total); + console.debug('DWC hit ratio', stats.dwc * 100 / stats.total); + console.debug('DWC / LRU hit ratio', `${stats.dwc / stats.lru * 100 | 0}%`); + console.debug('DWC ratio', dwc['partition']! * 100 / capacity | 0, dwc['LFU'].length * 100 / capacity | 0); + console.debug('DWC overlap', dwc['overlapLRU'], dwc['overlapLFU']); + assert(stats.dwc / stats.lru * 100 >>> 0 === 126); + }); + it('ratio zipf 100', function () { this.timeout(10 * 1e3); @@ -803,7 +836,7 @@ describe('Unit: lib/cache', () => { assert(dwc['partition']! * 100 / capacity >>> 0 === 0); }); - it('ratio uneven 1,000', function () { + it('ratio pzipf 1,000', function () { this.timeout(60 * 1e3); const capacity = 1000; @@ -814,25 +847,26 @@ describe('Unit: lib/cache', () => { const trials = capacity * 1000; const random = xorshift.random(1); const stats = new Stats(); + const log = { 0: 0 }; for (let i = 0; i < trials; ++i) { - const key = random() < 0.4 - ? random() * capacity * -1 | 0 - : random() * capacity * 10 | 0; + const key = Math.floor((random() * capacity) ** 2 / (10 * capacity / 100 | 0)); stats.lru += lru.get(key) ?? +lru.set(key, 1) & 0; stats.trc += trc.get(key) ?? +trc.set(key, 1) & 0; stats.dwc += dwc.get(key) ?? +dwc.set(key, 1) & 0; stats.total += 1; + key in log ? ++log[key] : log[key] = 1; } + //console.debug(Object.entries(log).sort((a, b) => b[1] - a[1]).map(e => [+e[0], e[1]])); assert(dwc['LRU'].length + dwc['LFU'].length === dwc['dict'].size); assert(dwc['dict'].size <= capacity); - console.debug('Cache uneven 1,000'); + console.debug('Cache pzipf 1,000'); console.debug('LRU hit ratio', stats.lru * 100 / stats.total); console.debug('TRC hit ratio', stats.trc * 100 / stats.total); console.debug('DWC hit ratio', stats.dwc * 100 / stats.total); console.debug('DWC / LRU hit ratio', `${stats.dwc / stats.lru * 100 | 0}%`); console.debug('DWC ratio', dwc['partition']! * 100 / capacity | 0, dwc['LFU'].length * 100 / capacity | 0); console.debug('DWC overlap', dwc['overlapLRU'], dwc['overlapLFU']); - assert(stats.dwc / stats.lru * 100 >>> 0 === 162); + assert(stats.dwc / stats.lru * 100 >>> 0 === 130); }); }); diff --git a/src/cache.ts b/src/cache.ts index a70de2d2..2717910d 100644 --- a/src/cache.ts +++ b/src/cache.ts @@ -79,7 +79,9 @@ https://github.com/ben-manes/caffeine/wiki/Efficiency /* # lru-cacheの最適化分析 -最適化前(@6)よりオブジェクト値において50-10%ほど高速化している。 +非常に高いヒット率では素朴な実装より高速化するがそれ以外の場合は低速化する。 +この最適化により素朴な実装より速くなる範囲が非常に狭く全体的には低速化している。 +巨大な配列を複数要するため空間オーバーヘッドも大きく消費メモリ基準では容量とヒット率が低下する。 ## Map値の数値化 @@ -88,11 +90,8 @@ getは変わらないため読み取り主体の場合効果が低い。 ## インデクスアクセス化 -個別の状態を個別のオブジェクトのプロパティに持たせると最適化されていないプロパティアクセスにより -低速化するためすべての状態を状態別の配列に格納しインデクスアクセスに変換することで高速化している。 -DWCはこの最適化を行っても状態数の多さに比例して増加したオーバーヘッドに相殺され効果を得られない。 -状態をオブジェクトの代わりに配列に入れても最適化されずプロパティ・インデクスとも二段のアクセスは -最適化されないと思われる。 +すべての状態を状態別の配列に格納しインデクスアクセスに変換することで高速化している。 +DWCはこの高速化を行っても状態数の多さに比例して増加したオーバーヘッドに相殺され効果を得られない。 ## TypedArray @@ -706,15 +705,19 @@ class Sweeper>> { private ratioWindow(): number { return ratio( this.window, - [this.currWindowHits, this.prevWindowHits], - [this.currWindowMisses, this.prevWindowMisses], + this.currWindowHits, + this.prevWindowHits, + this.currWindowMisses, + this.prevWindowMisses, 0); } private ratioRoom(): number { return ratio( this.room, - [this.currRoomHits, this.prevRoomHits], - [this.currRoomMisses, this.prevRoomMisses], + this.currRoomHits, + this.prevRoomHits, + this.currRoomMisses, + this.prevRoomMisses, 0); } private processing = false; @@ -779,16 +782,14 @@ class Sweeper>> { function ratio( window: number, - targets: readonly [number, number], - remains: readonly [number, number], + currHits: number, + prevHits: number, + currMisses: number, + prevMisses: number, offset: number, ): number { - assert(targets.length === 2); - assert(targets.length === remains.length); - const currHits = targets[0]; - const prevHits = targets[1]; - const currTotal = currHits + remains[0]; - const prevTotal = prevHits + remains[1]; + const currTotal = currHits + currMisses; + const prevTotal = prevHits + prevMisses; assert(currTotal <= window); const prevRate = prevHits && prevHits * 100 / prevTotal; const currRatio = currTotal * 100 / window - offset; @@ -824,13 +825,13 @@ function ratio2( } return hits * 10000 / total | 0; } -assert(ratio(10, [4, 0], [6, 0], 0) === 4000); -assert(ratio(10, [0, 4], [0, 6], 0) === 4000); -assert(ratio(10, [1, 4], [4, 6], 0) === 3000); -assert(ratio(10, [0, 4], [0, 6], 5) === 4000); -assert(ratio(10, [1, 2], [4, 8], 5) === 2000); -assert(ratio(10, [2, 2], [3, 8], 5) === 2900); -assert(ratio(10, [2, 0], [3, 0], 0) === 4000); +assert(ratio(10, 4, 0, 6, 0, 0) === 4000); +assert(ratio(10, 0, 4, 0, 6, 0) === 4000); +assert(ratio(10, 1, 4, 4, 6, 0) === 3000); +assert(ratio(10, 0, 4, 0, 6, 5) === 4000); +assert(ratio(10, 1, 2, 4, 8, 5) === 2000); +assert(ratio(10, 2, 2, 3, 8, 5) === 2900); +assert(ratio(10, 2, 0, 3, 0, 0) === 4000); assert(ratio2(10, [4, 0], [6, 0], 0) === 4000); assert(ratio2(10, [0, 4], [0, 6], 0) === 4000); assert(ratio2(10, [1, 4], [4, 6], 0) === 3000);