From 59df43e522686b7072880c6c6657b9dfe8622dee Mon Sep 17 00:00:00 2001 From: Ben Manes Date: Sat, 24 Jun 2023 22:07:04 -0700 Subject: [PATCH] Add the Quick Demotion - Lazy Promotion algorithm to the simulator --- .../caffeine/cache/simulator/Simulator.java | 28 +- .../cache/simulator/policy/PolicyActor.java | 6 +- .../cache/simulator/policy/Registry.java | 2 + .../policy/two_queue/QdlpPolicy.java | 287 ++++++++++++++++++ simulator/src/main/resources/reference.conf | 12 + 5 files changed, 330 insertions(+), 5 deletions(-) create mode 100644 simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/two_queue/QdlpPolicy.java diff --git a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/Simulator.java b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/Simulator.java index 31c5cb315e..908998e837 100644 --- a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/Simulator.java +++ b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/Simulator.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.Set; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Stream; @@ -33,6 +34,7 @@ import com.github.benmanes.caffeine.cache.simulator.policy.PolicyActor; import com.github.benmanes.caffeine.cache.simulator.policy.Registry; import com.google.common.base.Stopwatch; +import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; @@ -76,9 +78,7 @@ public void run() { broadcast(policies, trace); report(policies, trace.characteristics()); } catch (RuntimeException e) { - if (!Thread.currentThread().isInterrupted()) { - throw e; - } + throwError(policies, e); } } @@ -102,10 +102,11 @@ private void broadcast(List policies, TraceReader trace) { var remainder = List.copyOf(batch); for (var policy : policies) { policy.send(remainder); + policy.finish(); } var futures = policies.stream() - .map(PolicyActor::finish) + .map(PolicyActor::future) .toArray(CompletableFuture[]::new); CompletableFuture.allOf(futures).join(); } @@ -135,6 +136,25 @@ private ImmutableList getPolicyActors(Set character .collect(toImmutableList()); } + /** Throws the underlying cause for the simulation failure. */ + private void throwError(ImmutableList policies, RuntimeException e) { + if (!Thread.currentThread().isInterrupted()) { + throw e; + } + for (var policy : policies) { + if (policy.future().isCompletedExceptionally()) { + try { + policy.future().join(); + } catch (CompletionException error) { + Throwables.throwIfUnchecked(error.getCause()); + error.addSuppressed(e); + throw error; + } + } + } + throw e; + } + public static void main(String[] args) { Logger.getLogger("").setLevel(Level.WARNING); var simulator = new Simulator(ConfigFactory.load()); diff --git a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/PolicyActor.java b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/PolicyActor.java index 6decc378ce..3a2575c877 100644 --- a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/PolicyActor.java +++ b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/PolicyActor.java @@ -55,8 +55,12 @@ public void send(List events) { } /** Sends a shutdown signal after the pending messages are completed. */ - public CompletableFuture finish() { + public void finish() { submit(new Finish()); + } + + /** Return the future that signals the policy's completion. */ + public CompletableFuture future() { return future; } diff --git a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/Registry.java b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/Registry.java index 359336d193..8ea052b47b 100644 --- a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/Registry.java +++ b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/Registry.java @@ -72,6 +72,7 @@ import com.github.benmanes.caffeine.cache.simulator.policy.sketch.tinycache.TinyCachePolicy; import com.github.benmanes.caffeine.cache.simulator.policy.sketch.tinycache.TinyCacheWithGhostCachePolicy; import com.github.benmanes.caffeine.cache.simulator.policy.sketch.tinycache.WindowTinyCachePolicy; +import com.github.benmanes.caffeine.cache.simulator.policy.two_queue.QdlpPolicy; import com.github.benmanes.caffeine.cache.simulator.policy.two_queue.TuQueuePolicy; import com.github.benmanes.caffeine.cache.simulator.policy.two_queue.TwoQueuePolicy; import com.google.auto.value.AutoValue; @@ -171,6 +172,7 @@ private void registerSampled() { } private void registerTwoQueue() { + register(QdlpPolicy.class, QdlpPolicy::new); register(TuQueuePolicy.class, TuQueuePolicy::new); register(TwoQueuePolicy.class, TwoQueuePolicy::new); } diff --git a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/two_queue/QdlpPolicy.java b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/two_queue/QdlpPolicy.java new file mode 100644 index 0000000000..a7529d94f1 --- /dev/null +++ b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/two_queue/QdlpPolicy.java @@ -0,0 +1,287 @@ +/* + * Copyright 2023 Ben Manes. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.benmanes.caffeine.cache.simulator.policy.two_queue; + +import static com.google.common.base.Preconditions.checkState; + +import com.github.benmanes.caffeine.cache.simulator.BasicSettings; +import com.github.benmanes.caffeine.cache.simulator.policy.Policy.KeyOnlyPolicy; +import com.github.benmanes.caffeine.cache.simulator.policy.Policy.PolicySpec; +import com.github.benmanes.caffeine.cache.simulator.policy.PolicyStats; +import com.google.common.base.MoreObjects; +import com.typesafe.config.Config; + +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; + +/** + * The Quick Demotion - Lazy Promotion algorithm. This algorithm uses a probationary FIFO queue to + * evaluate whether a recent arrival should be admitted into the main region based on a frequency + * threshold. A rejected candidate is placed into a ghost cache, where a cache miss that hits in the + * ghost cache will cause the entry to be immediately promoted into the main region. This admission + * scheme is referred to as "quick demotion" by the authors. The main region uses an n-bit clock + * eviction policy, which is referred to as "lazy promotion" by the authors. + *

+ * This implementation is based on the code provided by the authors in their + * repository and described by the paper + * FIFO can be Better than LRU: the Power + * of Lazy Promotion and Quick Demotion and the accompanying + * slides. + * + * @author ben.manes@gmail.com (Ben Manes) + */ +@PolicySpec(name = "two-queue.Qdlp") +public final class QdlpPolicy implements KeyOnlyPolicy { + final Long2ObjectMap data; + final PolicyStats policyStats; + final Node headGhost; + final Node headFifo; + final Node headMain; + + final int mainMaximumEntryFrequency; + final int moveToMainThreshold; + final int maximumSize; + final int maxGhost; + final int maxFifo; + + int sizeGhost; + int sizeFifo; + int sizeMain; + + public QdlpPolicy(Config config) { + QdlpSettings settings = new QdlpSettings(config); + this.data = new Long2ObjectOpenHashMap<>(); + this.policyStats = new PolicyStats(name()); + this.headGhost = new Node(); + this.headFifo = new Node(); + this.headMain = new Node(); + + this.moveToMainThreshold = settings.moveToMainThreshold(); + this.maximumSize = Math.toIntExact(settings.maximumSize()); + this.maxFifo = (int) (maximumSize * settings.percentFifo()); + this.maxGhost = (int) (maximumSize * settings.percentGhost()); + this.mainMaximumEntryFrequency = settings.mainMaximumEntryFrequency(); + } + + @Override + public void record(long key) { + policyStats.recordOperation(); + Node node = data.get(key); + if (node == null) { + onMiss(key); + } else if (node.type == QueueType.GHOST) { + onGhostHit(node); + } else { + onHit(node); + } + } + + private void onHit(Node node) { + if (node.type == QueueType.FIFO) { + node.frequency++; + } else if (node.type == QueueType.MAIN) { + node.frequency = Math.min(node.frequency + 1, mainMaximumEntryFrequency); + } + policyStats.recordHit(); + } + + private void onGhostHit(Node node) { + policyStats.recordMiss(); + node.remove(); + sizeGhost--; + + node.appendToTail(headMain); + node.type = QueueType.MAIN; + sizeMain++; + evict(); + } + + private void onMiss(long key) { + Node node = new Node(key); + node.appendToTail(headFifo); + node.type = QueueType.FIFO; + policyStats.recordMiss(); + data.put(key, node); + sizeFifo++; + evict(); + + if (sizeFifo > maxFifo) { + Node promoted = headFifo.next; + promoted.remove(); + sizeFifo--; + + promoted.appendToTail(headMain); + promoted.type = QueueType.MAIN; + sizeMain++; + } + } + + private void evict() { + if ((sizeFifo + sizeMain) <= maximumSize) { + return; + } + policyStats.recordEviction(); + + if ((maxFifo == 0) || (sizeFifo == 0)) { + evictFromMain(); + return; + } + + Node candidate = headFifo.next; + int freq = candidate.frequency; + candidate.frequency = 0; + candidate.remove(); + + if (freq >= moveToMainThreshold) { + evictFromMain(); + candidate.appendToTail(headMain); + candidate.type = QueueType.MAIN; + sizeMain++; + sizeFifo--; + } else { + candidate.appendToTail(headGhost); + candidate.type = QueueType.GHOST; + candidate.frequency = 0; + sizeGhost++; + sizeFifo--; + + if (sizeGhost > maxGhost) { + var ghost = headGhost.next; + data.remove(ghost.key); + ghost.remove(); + sizeGhost--; + } + } + } + + private void evictFromMain() { + for (;;) { + Node victim = headMain.next; + if (victim.frequency == 0) { + data.remove(victim.key); + victim.remove(); + sizeMain--; + break; + } + victim.frequency--; + victim.moveToTail(headMain); + } + } + + @Override + public void finished() { + int maximum = (maximumSize + maxGhost); + checkState(data.size() <= maximum, "%s > %s", data.size(), maximum); + + long ghosts = data.values().stream().filter(node -> node.type == QueueType.GHOST).count(); + checkState(ghosts == sizeGhost, "ghosts: %s != %s", ghosts, sizeGhost); + checkState(ghosts <= maxGhost, "ghosts: %s > %s", ghosts, maxGhost); + } + + @Override + public PolicyStats stats() { + return policyStats; + } + + enum QueueType { + FIFO, + MAIN, + GHOST, + } + + static final class Node { + final long key; + + Node prev; + Node next; + QueueType type; + int frequency; + + Node() { + this.key = Long.MIN_VALUE; + this.prev = this; + this.next = this; + } + + Node(long key) { + this.key = key; + } + + /** Appends the node to the tail of the list. */ + public void appendToTail(Node head) { + checkState(prev == null); + checkState(next == null); + + Node tail = head.prev; + head.prev = this; + tail.next = this; + next = head; + prev = tail; + } + + /** Moves the node to the tail. */ + public void moveToTail(Node head) { + checkState(prev != null); + checkState(next != null); + + // unlink + prev.next = next; + next.prev = prev; + + // link + next = head; + prev = head.prev; + head.prev = this; + prev.next = this; + } + + /** Removes the node from the list. */ + public void remove() { + checkState(prev != null); + checkState(next != null); + + prev.next = next; + next.prev = prev; + prev = next = null; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("key", key) + .add("type", type) + .toString(); + } + } + + static final class QdlpSettings extends BasicSettings { + public QdlpSettings(Config config) { + super(config); + } + public double percentFifo() { + return config().getDouble("qdlp.percent-fifo"); + } + public double percentGhost() { + return config().getDouble("qdlp.percent-ghost"); + } + public int moveToMainThreshold() { + return config().getInt("qdlp.move-to-main-threshold"); + } + public int mainMaximumEntryFrequency() { + return config().getInt("qdlp.main-clock-maximum-frequency"); + } + } +} diff --git a/simulator/src/main/resources/reference.conf b/simulator/src/main/resources/reference.conf index f86da8b8af..201d7cb851 100644 --- a/simulator/src/main/resources/reference.conf +++ b/simulator/src/main/resources/reference.conf @@ -66,6 +66,7 @@ caffeine.simulator { # Policies based on the 2Q algorithm two-queue.TwoQueue, two-queue.TuQueue, + two-queue.Qdlp, # Policies based on a sketch algorithm sketch.WindowTinyLfu, @@ -176,6 +177,17 @@ caffeine.simulator { percent-warm = 0.33 } + qdlp { + # The percentage for the FIFO queue + percent-fifo = 0.10 + # The percentage for the GHOST queue + percent-ghost = 0.90 + # The promotion frequency threshold + move-to-main-threshold = 1 + # The n-bit clock frequency for the MAIN queue + main-clock-maximum-frequency = 1 + } + tiny-lfu { # CountMinSketch: count-min-4 (4-bit), count-min-64 (64-bit) # Table: random-table, tiny-table, perfect-table