From 1796be40dccb614892135a455d617d8b02021737 Mon Sep 17 00:00:00 2001 From: Ben Manes Date: Mon, 25 Dec 2023 15:10:06 -0800 Subject: [PATCH] Add Sieve and S3-FIFO to the simulator (#1417) --- gradle/libs.versions.toml | 2 + simulator/build.gradle.kts | 1 + .../cache/simulator/BasicSettings.java | 41 ++- .../cache/simulator/parser/TraceFormat.java | 4 + .../csv/LibCacheSimCsvTraceReader.java | 55 +++ .../parser/libcachesim/csv/package-info.java | 4 + .../LibCacheSimTwitterTraceReader.java | 58 +++ .../libcachesim/twitter/package-info.java | 4 + .../cache/simulator/policy/PolicyStats.java | 7 + .../cache/simulator/policy/Registry.java | 6 +- .../simulator/policy/linked/SievePolicy.java | 183 +++++++++ .../policy/two_queue/QdlpPolicy.java | 286 --------------- .../policy/two_queue/S3FifoPolicy.java | 347 ++++++++++++++++++ simulator/src/main/resources/reference.conf | 17 +- 14 files changed, 716 insertions(+), 299 deletions(-) create mode 100644 simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/parser/libcachesim/csv/LibCacheSimCsvTraceReader.java create mode 100644 simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/parser/libcachesim/csv/package-info.java create mode 100644 simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/parser/libcachesim/twitter/LibCacheSimTwitterTraceReader.java create mode 100644 simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/parser/libcachesim/twitter/package-info.java create mode 100644 simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/linked/SievePolicy.java delete mode 100644 simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/two_queue/QdlpPolicy.java create mode 100644 simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/two_queue/S3FifoPolicy.java diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c76b352dbe..18a6b29739 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -91,6 +91,7 @@ univocity-parsers = "2.9.1" versions = "0.50.0" xz = "1.9" ycsb = "0.17.0" +zero-allocation-hashing = "0.16" zstd = "1.5.5-11" [libraries] @@ -206,6 +207,7 @@ truth-java8 = { module = "com.google.truth.extensions:truth-java8-extension", ve univocity-parsers = { module = "com.univocity:univocity-parsers", version.ref = "univocity-parsers" } xz = { module = "org.tukaani:xz", version.ref = "xz" } ycsb = { module = "site.ycsb:core", version.ref = "ycsb" } +zero-allocation-hashing = { module = "net.openhft:zero-allocation-hashing", version.ref = "zero-allocation-hashing" } zstd = { module = "com.github.luben:zstd-jni", version.ref = "zstd" } [bundles] diff --git a/simulator/build.gradle.kts b/simulator/build.gradle.kts index 2c5baecdd2..d087b1f7ff 100644 --- a/simulator/build.gradle.kts +++ b/simulator/build.gradle.kts @@ -38,6 +38,7 @@ dependencies { implementation(libs.bundles.coherence) implementation(libs.bundles.slf4j.jdk) implementation(libs.univocity.parsers) + implementation(libs.zero.allocation.hashing) } application { diff --git a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/BasicSettings.java b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/BasicSettings.java index 1a8812f133..6091922264 100644 --- a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/BasicSettings.java +++ b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/BasicSettings.java @@ -23,13 +23,20 @@ import java.util.List; import java.util.Set; +import java.util.function.Function; +import java.util.regex.Pattern; + +import org.checkerframework.checker.nullness.qual.Nullable; import com.github.benmanes.caffeine.cache.simulator.admission.Admission; import com.github.benmanes.caffeine.cache.simulator.membership.FilterType; import com.github.benmanes.caffeine.cache.simulator.parser.TraceFormat; import com.github.benmanes.caffeine.cache.simulator.report.ReportFormat; import com.google.common.base.CaseFormat; +import com.google.common.primitives.Ints; +import com.google.common.primitives.Longs; import com.typesafe.config.Config; +import com.typesafe.config.ConfigException; /** * The simulator's configuration. A policy can extend this class as a convenient way to extract @@ -38,6 +45,8 @@ * @author ben.manes@gmail.com (Ben Manes) */ public class BasicSettings { + private static final Pattern NUMERIC_SEPARATOR = Pattern.compile("[_,]"); + private final Config config; public BasicSettings(Config config) { @@ -53,7 +62,7 @@ public ReportSettings report() { } public int randomSeed() { - return config().getInt("random-seed"); + return getFormattedInt("random-seed"); } public Set policies() { @@ -78,7 +87,7 @@ public TinyLfuSettings tinyLfu() { } public long maximumSize() { - return config().getLong("maximum-size"); + return getFormattedLong("maximum-size"); } public TraceSettings trace() { @@ -90,6 +99,30 @@ public Config config() { return config; } + /** Gets the quoted integer at the given path, ignoring comma and underscore separators */ + protected int getFormattedInt(String path) { + return parseFormattedNumber(path, config()::getInt, Ints::tryParse); + } + + /** Gets the quoted long at the given path, ignoring comma and underscore separators */ + protected long getFormattedLong(String path) { + return parseFormattedNumber(path, config()::getLong, Longs::tryParse); + } + + private T parseFormattedNumber(String path, + Function getter, Function tryParse) { + try { + return getter.apply(path); + } catch (ConfigException.Parse | ConfigException.WrongType e) { + var matcher = NUMERIC_SEPARATOR.matcher(config().getString(path)); + var value = tryParse.apply(matcher.replaceAll("")); + if (value == null) { + throw e; + } + return value; + } + } + public final class ActorSettings { public int mailboxSize() { return config().getInt("actor.mailbox-size"); @@ -183,10 +216,10 @@ public boolean enabled() { public final class TraceSettings { public long skip() { - return config().getLong("trace.skip"); + return getFormattedLong("trace.skip"); } public long limit() { - return config().getIsNull("trace.limit") ? Long.MAX_VALUE : config().getLong("trace.limit"); + return config().getIsNull("trace.limit") ? Long.MAX_VALUE : getFormattedLong("trace.limit"); } public boolean isFiles() { return config().getString("trace.source").equals("files"); diff --git a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/parser/TraceFormat.java b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/parser/TraceFormat.java index cf41cc285c..24e2ca1184 100644 --- a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/parser/TraceFormat.java +++ b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/parser/TraceFormat.java @@ -35,6 +35,8 @@ import com.github.benmanes.caffeine.cache.simulator.parser.glcache.GLCacheTraceReader; import com.github.benmanes.caffeine.cache.simulator.parser.gradle.GradleTraceReader; import com.github.benmanes.caffeine.cache.simulator.parser.kaggle.OutbrainTraceReader; +import com.github.benmanes.caffeine.cache.simulator.parser.libcachesim.csv.LibCacheSimCsvTraceReader; +import com.github.benmanes.caffeine.cache.simulator.parser.libcachesim.twitter.LibCacheSimTwitterTraceReader; import com.github.benmanes.caffeine.cache.simulator.parser.lirs.LirsTraceReader; import com.github.benmanes.caffeine.cache.simulator.parser.lrb.LrbTraceReader; import com.github.benmanes.caffeine.cache.simulator.parser.scarab.ScarabTraceReader; @@ -74,6 +76,8 @@ public enum TraceFormat { CORDA(CordaTraceReader::new), GL_CACHE(GLCacheTraceReader::new), GRADLE(GradleTraceReader::new), + LCS_TRACE(LibCacheSimCsvTraceReader::new), + LCS_TWITTER(LibCacheSimTwitterTraceReader::new), LIRS(LirsTraceReader::new), LRB(LrbTraceReader::new), OUTBRAIN(OutbrainTraceReader::new), diff --git a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/parser/libcachesim/csv/LibCacheSimCsvTraceReader.java b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/parser/libcachesim/csv/LibCacheSimCsvTraceReader.java new file mode 100644 index 0000000000..50bafb4d5a --- /dev/null +++ b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/parser/libcachesim/csv/LibCacheSimCsvTraceReader.java @@ -0,0 +1,55 @@ +/* + * 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.parser.libcachesim.csv; + +import static com.github.benmanes.caffeine.cache.simulator.policy.Policy.Characteristic.WEIGHTED; + +import java.util.Set; +import java.util.stream.Stream; + +import com.github.benmanes.caffeine.cache.simulator.parser.TextTraceReader; +import com.github.benmanes.caffeine.cache.simulator.policy.AccessEvent; +import com.github.benmanes.caffeine.cache.simulator.policy.Policy.Characteristic; + +/** + * A reader for the data/trace.csv file provided by the authors of + * libCacheSim. + * + * @author ben.manes@gmail.com (Ben Manes) + */ +public final class LibCacheSimCsvTraceReader extends TextTraceReader { + + public LibCacheSimCsvTraceReader(String filePath) { + super(filePath); + } + + @Override + public Set characteristics() { + return Set.of(WEIGHTED); + } + + @Override + public Stream events() { + return lines() + .skip(1) + .map(line -> line.split(",")) + .map(array -> { + long key = Long.parseLong(array[4]); + int weight = Integer.parseInt(array[3]); + return AccessEvent.forKeyAndWeight(key, weight); + }); + } +} diff --git a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/parser/libcachesim/csv/package-info.java b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/parser/libcachesim/csv/package-info.java new file mode 100644 index 0000000000..c3ee916633 --- /dev/null +++ b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/parser/libcachesim/csv/package-info.java @@ -0,0 +1,4 @@ +@CheckReturnValue +package com.github.benmanes.caffeine.cache.simulator.parser.libcachesim.csv; + +import com.google.errorprone.annotations.CheckReturnValue; diff --git a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/parser/libcachesim/twitter/LibCacheSimTwitterTraceReader.java b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/parser/libcachesim/twitter/LibCacheSimTwitterTraceReader.java new file mode 100644 index 0000000000..b266a52db7 --- /dev/null +++ b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/parser/libcachesim/twitter/LibCacheSimTwitterTraceReader.java @@ -0,0 +1,58 @@ +/* + * 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.parser.libcachesim.twitter; + +import static com.github.benmanes.caffeine.cache.simulator.policy.Policy.Characteristic.WEIGHTED; + +import java.util.Set; +import java.util.stream.Stream; + +import com.github.benmanes.caffeine.cache.simulator.parser.TextTraceReader; +import com.github.benmanes.caffeine.cache.simulator.policy.AccessEvent; +import com.github.benmanes.caffeine.cache.simulator.policy.Policy.Characteristic; + +import net.openhft.hashing.LongHashFunction; + +/** + * A reader for the data/twitter_cluster52.csv file provided by the authors of + * libCacheSim. + * + * @author ben.manes@gmail.com (Ben Manes) + */ +public final class LibCacheSimTwitterTraceReader extends TextTraceReader { + + public LibCacheSimTwitterTraceReader(String filePath) { + super(filePath); + } + + @Override + public Set characteristics() { + return Set.of(WEIGHTED); + } + + @Override + public Stream events() { + var hasher = LongHashFunction.xx3(); + return lines() + .skip(1) + .map(line -> line.split(", ")) + .map(array -> { + long key = hasher.hashChars(array[1]); + int weight = Integer.parseInt(array[2]); + return AccessEvent.forKeyAndWeight(key, weight); + }); + } +} diff --git a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/parser/libcachesim/twitter/package-info.java b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/parser/libcachesim/twitter/package-info.java new file mode 100644 index 0000000000..2d6ff32c80 --- /dev/null +++ b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/parser/libcachesim/twitter/package-info.java @@ -0,0 +1,4 @@ +@CheckReturnValue +package com.github.benmanes.caffeine.cache.simulator.parser.libcachesim.twitter; + +import com.google.errorprone.annotations.CheckReturnValue; diff --git a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/PolicyStats.java b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/PolicyStats.java index ff52f2f608..06f1005bcd 100644 --- a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/PolicyStats.java +++ b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/PolicyStats.java @@ -69,6 +69,7 @@ public PolicyStats(String format, Object... args) { addMetric(Metric.of("Policy", (Supplier) this::name, OBJECT, true)); addMetric(Metric.of("Hit Rate", (DoubleSupplier) this::hitRate, PERCENT, true)); + addMetric(Metric.of("Miss Rate", (DoubleSupplier) this::missRate, PERCENT, true)); addMetric(Metric.of("Hits", (LongSupplier) this::hitCount, NUMBER, true)); addMetric(Metric.of("Misses", (LongSupplier) this::missCount, NUMBER, true)); addMetric(Metric.of("Requests", (LongSupplier) this::requestCount, NUMBER, true)); @@ -87,6 +88,12 @@ public PolicyStats(String format, Object... args) { .name("Weighted Hit Rate") .type(PERCENT) .build()); + addMetric(Metric.builder() + .value((DoubleSupplier) this::weightedMissRate) + .addCharacteristic(WEIGHTED) + .name("Weighted Miss Rate") + .type(PERCENT) + .build()); addPercentMetric("Adaption", this::percentAdaption); addMetric("Average Miss Penalty", this::averageMissPenalty); addMetric("Average Penalty", this::avergePenalty); 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 ebe32aef98..28207f6ab7 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 @@ -49,6 +49,7 @@ import com.github.benmanes.caffeine.cache.simulator.policy.linked.MultiQueuePolicy; import com.github.benmanes.caffeine.cache.simulator.policy.linked.S4LruPolicy; import com.github.benmanes.caffeine.cache.simulator.policy.linked.SegmentedLruPolicy; +import com.github.benmanes.caffeine.cache.simulator.policy.linked.SievePolicy; import com.github.benmanes.caffeine.cache.simulator.policy.opt.ClairvoyantPolicy; import com.github.benmanes.caffeine.cache.simulator.policy.opt.UnboundedPolicy; import com.github.benmanes.caffeine.cache.simulator.policy.product.Cache2kPolicy; @@ -71,7 +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.S3FifoPolicy; 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; @@ -158,6 +159,7 @@ private void registerLinked() { registerMany(policy.label(), FrequentlyUsedPolicy.class, config -> FrequentlyUsedPolicy.policies(config, policy)); } + register(SievePolicy.class, SievePolicy::new); registerMany(S4LruPolicy.class, S4LruPolicy::policies); register(MultiQueuePolicy.class, MultiQueuePolicy::new); registerMany(SegmentedLruPolicy.class, SegmentedLruPolicy::policies); @@ -171,7 +173,7 @@ private void registerSampled() { } private void registerTwoQueue() { - register(QdlpPolicy.class, QdlpPolicy::new); + register(S3FifoPolicy.class, S3FifoPolicy::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/linked/SievePolicy.java b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/linked/SievePolicy.java new file mode 100644 index 0000000000..b62bce9394 --- /dev/null +++ b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/linked/SievePolicy.java @@ -0,0 +1,183 @@ +/* + * 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.linked; + +import static com.github.benmanes.caffeine.cache.simulator.policy.Policy.Characteristic.WEIGHTED; +import static com.google.common.base.Preconditions.checkState; + +import com.github.benmanes.caffeine.cache.simulator.BasicSettings; +import com.github.benmanes.caffeine.cache.simulator.policy.AccessEvent; +import com.github.benmanes.caffeine.cache.simulator.policy.Policy; +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 SIEVE algorithm. This algorithm modifies the classic Clock policy (aka Second Chance) to + * decouple the clock hand from the insertion point, so that recent arrival are more likely to be + * evicted early (as potential one-hit wonders). + *

+ * This implementation is based on the code provided by the authors at + * + * SIEVE is simpler than LRU and described by the paper + * SIEVE: an Efficient Turn-Key + * Eviction Algorithm for Web Caches. + * + * @author ben.manes@gmail.com (Ben Manes) + */ +@PolicySpec(name = "linked.Sieve", characteristics = WEIGHTED) +public final class SievePolicy implements Policy { + final Long2ObjectMap data; + final PolicyStats policyStats; + final long maximumSize; + + Node head; + Node tail; + Node hand; + long size; + + public SievePolicy(Config config) { + this.data = new Long2ObjectOpenHashMap<>(); + this.policyStats = new PolicyStats(name()); + var settings = new BasicSettings(config); + this.maximumSize = settings.maximumSize(); + } + + @Override + public void record(AccessEvent event) { + policyStats.recordOperation(); + Node node = data.get(event.key()); + if (node == null) { + onMiss(event); + } else { + onHit(event, node); + } + } + + private void onHit(AccessEvent event, Node node) { + policyStats.recordWeightedHit(event.weight()); + size += (event.weight() - node.weight); + node.weight = event.weight(); + node.visited = true; + + while (size >= maximumSize) { + evict(); + } + } + + private void onMiss(AccessEvent event) { + if (event.weight() > maximumSize) { + policyStats.recordWeightedMiss(event.weight()); + return; + } + while ((size + event.weight()) >= maximumSize) { + evict(); + } + policyStats.recordWeightedMiss(event.weight()); + var node = new Node(event.key(), event.weight()); + data.put(event.key(), node); + size += event.weight(); + addToHead(node); + } + + private void evict() { + var victim = (hand == null) ? tail : hand; + while ((victim != null) && victim.visited) { + victim.visited = false; + victim = (victim.prev == null) ? tail : victim.prev; + policyStats.recordOperation(); + } + if (victim != null) { + policyStats.recordEviction(); + data.remove(victim.key); + size -= victim.weight; + hand = victim.prev; + remove(victim); + } + } + + private void addToHead(Node node) { + checkState(node.prev == null); + checkState(node.next == null); + + node.next = head; + if (head != null) { + head.prev = node; + } + head = node; + if (tail == null) { + tail = node; + } + } + + private void remove(Node node) { + if (node.prev != null) { + node.prev.next = node.next; + } else { + head = node.next; + } + if (node.next != null) { + node.next.prev = node.prev; + } else { + tail = node.prev; + } + } + + @Override + public void finished() { + checkState(size <= maximumSize, "%s > %s", size, maximumSize); + long weightedSize = data.values().stream().mapToLong(node -> node.weight).sum(); + checkState(weightedSize == size, "%s != %s", weightedSize, size); + } + + @Override + public PolicyStats stats() { + return policyStats; + } + + static final class Node { + final long key; + + Node prev; + Node next; + int weight; + boolean visited; + + Node() { + this.key = Long.MIN_VALUE; + this.prev = this; + this.next = this; + } + + Node(long key, int weight) { + this.key = key; + this.weight = weight; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("key", key) + .add("weight", weight) + .add("visited", visited) + .toString(); + } + } +} 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 deleted file mode 100644 index 11eb3054e2..0000000000 --- a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/two_queue/QdlpPolicy.java +++ /dev/null @@ -1,286 +0,0 @@ -/* - * 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(); - sizeFifo--; - - if (freq >= moveToMainThreshold) { - evictFromMain(); - candidate.appendToTail(headMain); - candidate.type = QueueType.MAIN; - sizeMain++; - } else { - candidate.appendToTail(headGhost); - candidate.type = QueueType.GHOST; - candidate.frequency = 0; - sizeGhost++; - - 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/java/com/github/benmanes/caffeine/cache/simulator/policy/two_queue/S3FifoPolicy.java b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/two_queue/S3FifoPolicy.java new file mode 100644 index 0000000000..563d503dc7 --- /dev/null +++ b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/two_queue/S3FifoPolicy.java @@ -0,0 +1,347 @@ +/* + * 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.github.benmanes.caffeine.cache.simulator.policy.Policy.Characteristic.WEIGHTED; +import static com.google.common.base.Preconditions.checkState; + +import java.util.LinkedHashSet; +import java.util.function.IntConsumer; + +import com.github.benmanes.caffeine.cache.simulator.BasicSettings; +import com.github.benmanes.caffeine.cache.simulator.policy.AccessEvent; +import com.github.benmanes.caffeine.cache.simulator.policy.Policy; +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.google.errorprone.annotations.CanIgnoreReturnValue; +import com.typesafe.config.Config; + +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; + +/** + * The Simple and Scalable caching with Static FIFO queues 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. The small and main regions uses an n-bit clock eviction policy. + *

+ * This implementation is based on the code provided by the authors in their + * repository and the pseudo code in their + * paper FIFO queues are all you need for + * cache eviction. It does not use any of the paper's proposed adaptations for space saving or + * concurrency because that may impact the hit rate. An implementor is encouraged to use a more + * optimal structure than the reference's that is mimicked here. + * + * @author ben.manes@gmail.com (Ben Manes) + */ +@PolicySpec(name = "two-queue.S3Fifo", characteristics = WEIGHTED) +public final class S3FifoPolicy implements Policy { + final Long2ObjectMap dataGhost; + final Long2ObjectMap dataSmall; + final Long2ObjectMap dataMain; + final PolicyStats policyStats; + final Node sentinelGhost; + final Node sentinelSmall; + final Node sentinelMain; + + final int moveToMainThreshold; + final int maxFrequency; + final long maximumSize; + final long maxGhost; + final long maxSmall; + final long maxMain; + + long sizeGhost; + long sizeSmall; + long sizeMain; + + public S3FifoPolicy(Config config) { + this.dataGhost = new Long2ObjectOpenHashMap<>(); + this.dataSmall = new Long2ObjectOpenHashMap<>(); + this.dataMain = new Long2ObjectOpenHashMap<>(); + this.policyStats = new PolicyStats(name()); + this.sentinelGhost = new Node(); + this.sentinelSmall = new Node(); + this.sentinelMain = new Node(); + + var settings = new S3FifoSettings(config); + this.maximumSize = settings.maximumSize(); + this.maxFrequency = settings.maximumFrequency(); + this.moveToMainThreshold = settings.moveToMainThreshold(); + this.maxSmall = (long) (maximumSize * settings.percentSmall()); + this.maxGhost = (long) (maximumSize * settings.percentGhost()); + this.maxMain = maximumSize - maxSmall; + } + + @Override + public void record(AccessEvent event) { + Node node; + if ((node = dataSmall.get(event.key())) != null) { + onHit(event, node, change -> sizeSmall += change); + } else if ((node = dataMain.get(event.key())) != null) { + onHit(event, node, change -> sizeMain += change); + } else if (event.weight() > maximumSize) { + policyStats.recordWeightedMiss(event.weight()); + } else { + policyStats.recordWeightedMiss(event.weight()); + node = insert(event); + node.frequency = 0; + } + policyStats.recordOperation(); + } + + private void onHit(AccessEvent event, Node node, IntConsumer sizeAdjuster) { + node.frequency = Math.min(node.frequency + 1, maxFrequency); + sizeAdjuster.accept(event.weight() - node.weight); + node.weight = event.weight(); + + policyStats.recordWeightedHit(event.weight()); + while ((sizeSmall + sizeMain) >= maximumSize) { + evict(); + } + } + + private Node insert(AccessEvent event) { + while ((sizeSmall + sizeMain + event.weight()) > maximumSize) { + evict(); + } + var ghost = dataGhost.remove(event.key()); + if (ghost != null) { + ghost.remove(); + sizeGhost -= ghost.weight; + return insertMain(event.key(), event.weight()); + } else { + return insertSmall(event.key(), event.weight()); + } + } + + @CanIgnoreReturnValue + private Node insertMain(long key, int weight) { + var node = new Node(key, weight); + node.appendAtHead(sentinelMain); + dataMain.put(key, node); + sizeMain += node.weight; + return node; + } + + private Node insertSmall(long key, int weight) { + var node = new Node(key, weight); + node.appendAtHead(sentinelSmall); + dataSmall.put(key, node); + sizeSmall += node.weight; + return node; + } + + @CanIgnoreReturnValue + private Node insertGhost(long key, int weight) { + // Bound the number of non-resident entries. While not included in the paper's pseudo code, the + // author's reference implementation adds a similar constraint to avoid uncontrolled growth. + while ((sizeGhost + weight) > maxGhost) { + evictFromGhost(); + } + + var node = new Node(key, weight); + node.appendAtHead(sentinelGhost); + dataGhost.put(key, node); + sizeGhost += node.weight; + return node; + } + + private void evict() { + if (sizeSmall >= maxSmall) { + evictFromSmall(); + } else { + evictFromMain(); + } + } + + private void evictFromSmall() { + boolean evicted = false; + while (!evicted && !dataSmall.isEmpty()) { + var victim = sentinelSmall.prev; + policyStats.recordOperation(); + if (victim.frequency > moveToMainThreshold) { + insertMain(victim.key, victim.weight); + if (sizeMain > maxMain) { + evictFromMain(); + } + } else { + insertGhost(victim.key, victim.weight); + evicted = true; + } + policyStats.recordEviction(); + dataSmall.remove(victim.key); + sizeSmall -= victim.weight; + victim.remove(); + } + } + + private void evictFromMain() { + boolean evicted = false; + while (!evicted && !dataMain.isEmpty()) { + var victim = sentinelMain.prev; + policyStats.recordOperation(); + if (victim.frequency > 0) { + victim.moveToHead(sentinelMain); + victim.frequency--; + } else { + policyStats.recordEviction(); + dataMain.remove(victim.key); + sizeMain -= victim.weight; + victim.remove(); + evicted = true; + } + } + } + + private void evictFromGhost() { + if (!dataGhost.isEmpty()) { + var victim = sentinelGhost.prev; + dataGhost.remove(victim.key); + sizeGhost -= victim.weight; + victim.remove(); + } + } + + @Override + public void finished() { + long actualGhost = dataGhost.values().stream().mapToLong(node -> node.weight).sum(); + checkState(actualGhost == sizeGhost, "Ghost: %s != %s", actualGhost, sizeGhost); + checkState(sizeGhost <= maxGhost, "Ghost: %s > %s", sizeGhost, maxGhost); + checkLinks("Ghost", dataGhost, sentinelGhost); + + long actualSmall = dataSmall.values().stream().mapToLong(node -> node.weight).sum(); + checkState(actualSmall == sizeSmall, "Small: %s != %s", actualSmall, sizeSmall); + checkLinks("Small", dataSmall, sentinelSmall); + + long actualMain = dataMain.values().stream().mapToLong(node -> node.weight).sum(); + checkState(actualMain == sizeMain, "Main: %s != %s", actualMain, sizeMain); + checkLinks("Main", dataMain, sentinelMain); + + long actualSize = sizeSmall + sizeMain; + checkState(actualSize <= maximumSize, "Total: %s > %s", actualSize, maximumSize); + } + + private void checkLinks(String label, Long2ObjectMap data, Node sentinel) { + var forwards = new LinkedHashSet(); + for (var node = sentinel.next; node != sentinel; node = node.next) { + checkState(node == data.get(node.key), "%s: %s != %s", label, node, data.get(node.key)); + checkState(forwards.add(node), "%s: loop detected %s", label, forwards); + } + checkState(forwards.size() == data.size(), "%s (forwards): %s != %s", + label, forwards.size(), data.size()); + + var backwards = new LinkedHashSet(); + for (var node = sentinel.prev; node != sentinel; node = node.prev) { + checkState(node == data.get(node.key), "%s: %s != %s", label, node, data.get(node.key)); + checkState(backwards.add(node), "%s: loop detected %s", label, backwards); + } + checkState(backwards.size() == data.size(), "%s (backwards): %s != %s", + label, backwards.size(), data.size()); + } + + @Override + public PolicyStats stats() { + return policyStats; + } + + static final class Node { + final long key; + + Node prev; + Node next; + int weight; + int frequency; + + Node() { + this.key = Long.MIN_VALUE; + this.prev = this; + this.next = this; + } + + Node(long key, int weight) { + this.key = key; + this.weight = weight; + } + + /** Appends the node as the head of the list. */ + public void appendAtHead(Node sentinel) { + checkState(prev == null); + checkState(next == null); + + Node head = sentinel.next; + sentinel.next = this; + head.prev = this; + next = head; + prev = sentinel; + } + + /** Moves the node to the tail. */ + public void moveToHead(Node sentinel) { + checkState(prev != null); + checkState(next != null); + + // unlink + prev.next = next; + next.prev = prev; + + // link + next = sentinel.next; + sentinel.next = this; + next.prev = this; + prev = sentinel; + } + + /** 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("weight", weight) + .add("freq", frequency) + .toString(); + } + } + + static final class S3FifoSettings extends BasicSettings { + public S3FifoSettings(Config config) { + super(config); + } + public double percentSmall() { + return config().getDouble("s3fifo.percent-small"); + } + public double percentGhost() { + return config().getDouble("s3fifo.percent-ghost"); + } + public int moveToMainThreshold() { + return config().getInt("s3fifo.move-to-main-threshold"); + } + public int maximumFrequency() { + return config().getInt("s3fifo.maximum-frequency"); + } + } +} diff --git a/simulator/src/main/resources/reference.conf b/simulator/src/main/resources/reference.conf index 4a6bf540f6..2b236111f4 100644 --- a/simulator/src/main/resources/reference.conf +++ b/simulator/src/main/resources/reference.conf @@ -46,6 +46,7 @@ caffeine.simulator { linked.Fifo, linked.Clock, linked.S4Lru, + linked.Sieve, linked.MultiQueue, linked.SegmentedLru, @@ -66,7 +67,7 @@ caffeine.simulator { # Policies based on the 2Q algorithm two-queue.TwoQueue, two-queue.TuQueue, - two-queue.Qdlp, + two-queue.S3Fifo, # Policies based on a sketch algorithm sketch.WindowTinyLfu, @@ -176,15 +177,15 @@ caffeine.simulator { percent-warm = 0.33 } - qdlp { - # The percentage for the FIFO queue - percent-fifo = 0.10 - # The percentage for the GHOST queue + s3fifo { + # The percentage for the S queue + percent-small = 0.10 + # The percentage for the G 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 + # The n-bit clock frequency for the S and M queues + maximum-frequency = 3 } tiny-lfu { @@ -494,6 +495,8 @@ caffeine.simulator { # corda: format of Corda traces # gl-cache: format from the authors of the GL-Cache algorithm # gradle: format from the authors of the Gradle build tool + # lcs_trace: format from the authors of libCacheSim + # lcs_twitter: format from the authors of libCacheSim # lirs: format from the authors of the LIRS algorithm # lrb: format from the authors of the LRB algorithm # outbrain: format of Outbrain's trace provided on Kaggle