From b498408d9c36c2d29f75b1d73695f879639ae795 Mon Sep 17 00:00:00 2001 From: chanyoung Date: Sun, 16 May 2021 17:58:02 +0900 Subject: [PATCH] Add simplified ClockPro policy --- .../cache/simulator/policy/Registry.java | 2 + .../policy/irr/ClockProSimplePolicy.java | 402 ++++++++++++++++++ simulator/src/main/resources/reference.conf | 1 + 3 files changed, 405 insertions(+) create mode 100644 simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/irr/ClockProSimplePolicy.java 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 6d42929b10..892410125d 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 @@ -38,6 +38,7 @@ import com.github.benmanes.caffeine.cache.simulator.policy.greedy_dual.GdsfPolicy; import com.github.benmanes.caffeine.cache.simulator.policy.irr.ClockProPlusPolicy; import com.github.benmanes.caffeine.cache.simulator.policy.irr.ClockProPolicy; +import com.github.benmanes.caffeine.cache.simulator.policy.irr.ClockProSimplePolicy; import com.github.benmanes.caffeine.cache.simulator.policy.irr.DClockPolicy; import com.github.benmanes.caffeine.cache.simulator.policy.irr.FrdPolicy; import com.github.benmanes.caffeine.cache.simulator.policy.irr.HillClimberFrdPolicy; @@ -206,6 +207,7 @@ private void registerIrr() { register(LirsPolicy.class, LirsPolicy::new); register(ClockProPolicy.class, ClockProPolicy::new); register(ClockProPlusPolicy.class, ClockProPlusPolicy::new); + register(ClockProSimplePolicy.class, ClockProSimplePolicy::new); registerMany(DClockPolicy.class, DClockPolicy::policies); } diff --git a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/irr/ClockProSimplePolicy.java b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/irr/ClockProSimplePolicy.java new file mode 100644 index 0000000000..33cd385a43 --- /dev/null +++ b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/irr/ClockProSimplePolicy.java @@ -0,0 +1,402 @@ +/* + * Copyright 2015 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.irr; + +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.google.common.primitives.Ints; +import com.typesafe.config.Config; +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; + +import static com.google.common.base.Preconditions.checkState; + +/** + * The ClockPro algorithm. This algorithm differs from LIRS by replacing the LRU stacks with Clock + * (Second Chance) policy. This allows cache hits to be performed concurrently at the cost of a + * global lock on a miss and a worst case O(n) eviction when the queue is scanned. + *

+ * ClockPro uses three hands that scan the queue. The hot hand points to the largest recency, the + * cold hand to the cold entry furthest from the hot hand, and the test hand to the last cold entry + * in the test period. This policy is adaptive by adjusting the percentage of hot and cold entries + * that may reside in the cache. It uses non-resident (ghost) entries to retain additional history, + * which are removed during the test hand's scan. The algorithm is explained by the authors in + * CLOCK-Pro: An + * Effective Improvement of the CLOCK Replacement and + * Clock-Pro: An Effective Replacement in OS + * Kernel. + * + * This implementation works exactly like ClockPro, but pursues the simplicity of the code. + * It divides a single list of ClockPro into three lists: hot, cold, and non-resident. + * For maintaining a test period of each entry, it uses epoch. + * + * @author ben.manes@gmail.com (Ben Manes) + * @author park910113@gmail.com (Chanyoung Park) + */ +@PolicySpec(name = "irr.ClockProSimple") +public final class ClockProSimplePolicy implements KeyOnlyPolicy { + private final Long2ObjectMap data; + private final PolicyStats policyStats; + + private final Node headHot; + private final Node headCold; + private final Node headNonResident; + + // Maximum number of resident entries (hot + resident cold) + private final int maxSize; + private final int minColdSize; + private final int maxColdSize; + + private int sizeHot; + private int sizeCold; + private int sizeNR; + + // To know the order of entries, epoch is used. The epoch is incremented by 1 when a new entry is + // inserted, or when an existing entry has been re-accessed and moved to the head. The epoch is + // used to determine whether an entry's test period has expired or not. Use int64 type or + // consider handling integer overflow. + // + // For example, integer overflow can be handled by: + // // Newer returns true if x is newer epoch than y, otherwise return false. This method is safe + // // from integer overflow when 1) epoch data type is signed numeric type, and 2) can represent + // // a number greater than the maximum number of cache entries * 2. + // private boolean newer(long x, long y) { + // if ((x ^ y) < 0 && epoch < 0) { + // // If the signs of x and y are different and the current epoch + // // is negative, the negative epoch is always newer. + // return !(x > y); + // } else { + // return x > y; + // } + // } + private long epoch; + + // Target number of resident cold entries (adaptive): + // - increases when test entry gets a hit + // - decreases when test entry is removed + private int coldTarget; + + // Enable to print out the internal state + static final boolean debug = false; + + public ClockProSimplePolicy(Config config) { + BasicSettings settings = new BasicSettings(config); + this.maxSize = Ints.checkedCast(settings.maximumSize()); + this.minColdSize = this.maxSize / 100; + this.maxColdSize = this.maxSize - (this.maxSize / 100); + this.policyStats = new PolicyStats(name()); + this.data = new Long2ObjectOpenHashMap<>(); + this.headHot = new Node(); + this.headCold = new Node(); + this.headNonResident = new Node(); + this.epoch = Long.MIN_VALUE; + } + + @Override + public PolicyStats stats() { + return policyStats; + } + + @Override + public void finished() { + if (debug) { + printClock(); + } + int cold = (int) data.values().stream() + .filter(node -> node.status == Status.COLD) + .count(); + int hot = (int) data.values().stream() + .filter(node -> node.status == Status.HOT) + .count(); + int nonResident = (int) data.values().stream() + .filter(node -> node.status == Status.NR) + .count(); + + checkState(cold == sizeCold, + "Cold: expected %s but was %s", sizeCold, cold); + checkState(hot == sizeHot, + "Hot: expected %s but was %s", sizeHot, hot); + checkState(nonResident == sizeNR, + "NonResident: expected %s but was %s", sizeNR, nonResident); + checkState(data.size() == (cold + hot + nonResident)); + checkState(cold + hot <= maxSize); + checkState(nonResident <= maxSize); + } + + @Override + public void record(long key) { + Node node = data.get(key); + if (node == null) { + onMiss(key); + } else if (node.status == Status.HOT || node.status == Status.COLD) { + onHit(node); + } else if (node.status == Status.NR) { + onNonResidentMiss(node); + } else { + throw new IllegalStateException(); + } + } + + private void onHit(Node node) { + policyStats.recordOperation(); + policyStats.recordHit(); + node.marked = true; + } + + private void onMiss(long key) { + policyStats.recordOperation(); + policyStats.recordMiss(); + epoch++; + Node node = new Node(key, epoch); + node.status = Status.COLD; + node.link(headCold); + data.put(key, node); + sizeCold++; + evict(); + } + + // Prune removes all non-resident entries whose test period has expired. + private void prune() { + while (sizeNR > 0 && !inTestPeriod(headNonResident.prev)) { + scanNonResident(); + } + } + + private void onNonResidentMiss(Node node) { + policyStats.recordOperation(); + policyStats.recordMiss(); + node.unlink(); + sizeNR--; + if (canPromote(node)) { + node.status = Status.HOT; + node.link(headHot); + sizeHot++; + } else { + node.status = Status.COLD; + node.link(headCold); + sizeCold++; + } + node.epoch = epoch; + evict(); + } + + private void evict() { + policyStats.recordEviction(); + while (maxSize < sizeCold + sizeHot) { + if (sizeCold > 0) { + scanCold(); + } else { + scanHot(epoch); + } + } + prune(); + } + + private boolean canPromote(Node candidate) { + // Only entries in its test period can be considered to promote. + if (!inTestPeriod(candidate)) { + return false; + } + // The candidate cold entry was re-accessed during its test period, so we increment coldTarget. + adjustColdTarget(+1); + while (sizeHot > 0 && sizeHot >= maxSize - coldTarget) { + // Candidate's test period has been expired while scanning hot entries. Reject the promotion. + if (!scanHot(candidate.epoch)) { + return false; + } + } + return inTestPeriod(candidate); + } + + private void scanCold() { + policyStats.recordOperation(); + Node victim = headCold.prev; + victim.unlink(); + if (victim.marked) { + // If its bit is set and it is in its test period, we consider this entry as a candidate for + // promotion to hot, because an access during the test period indicates a competitively + // small reuse distance. We scans hot entries for finding a hot entry with a longer reuse + // distance than the candidate cold entry. If we failed to find a hot entry with a longer + // reuse distance than the candidate, or the candidate test period is expired, we reset its + // reference bit and move it to the list head, and grant a new test period by renewing the + // epoch. + victim.marked = false; + if (canPromote(victim)) { + victim.status = Status.HOT; + victim.link(headHot); + sizeCold--; + sizeHot++; + } else { + victim.link(headCold); + } + epoch++; + victim.epoch = epoch; + } else { + // If the reference bit of the cold entry is unset, we replace the cold entry for a free + // space. If the replaced cold entry is in its test period, then it will remain in the list + // as a non-resident cold entry until it runs out of its test period. If the replaced cold + // entry is not in its test period, we move it out of the clock. + sizeCold--; + if (inTestPeriod(victim)) { + victim.status = Status.NR; + victim.link(headNonResident); + sizeNR++; + } else { + data.remove(victim.key); + } + // We keep track the number of non-resident cold entries. Once the number exceeds the limit, + // we terminate the test period of the oldest non-resident entry. + while (sizeNR > maxSize) { + scanNonResident(); + } + } + } + + // ScanHot demotes a hot entry between the oldest hot entry's epoch and the given epoch. + // If the demotion was successful it returns true, otherwise it returns false. + private boolean scanHot(long epoch) { + for (Node victim = headHot.prev; victim.epoch <= epoch; victim = headHot.prev) { + policyStats.recordOperation(); + victim.unlink(); + // If the reference bit of the hot entry is unset, we can simply change its status and link + // to the head of cold list. However, if the bit is set, which indicates the entry has been + // re-accessed, we spare this entry, reset its reference bit and keep it as a hot entry. + // This is because the actual access time of the hot entry could be earlier than the cold + // entry. Then we move the hand forward and do the same on the hot entries with their bits + // set until the hand encounters a hot entry with a reference bit of zero. Then the hot + // entry turns into a cold entry. + if (victim.marked) { + victim.marked = false; + victim.link(headHot); + epoch++; + victim.epoch = epoch; + } else { + victim.status = Status.COLD; + victim.link(headCold); + sizeHot--; + sizeCold++; + return true; + } + } + return false; + } + + private void scanNonResident() { + policyStats.recordOperation(); + // We terminate the test period of the non-resident cold entry, and also remove it from the + // clock. Because the cold entry has used up its test period without a re-access and has no + // chance to turn into a hot entry with its next access. + Node victim = headNonResident.prev; + victim.unlink(); + data.remove(victim.key); + sizeNR--; + // If a cold entry passes its test period without a re-access, we decrement coldTarget. + adjustColdTarget(-1); + } + + private void adjustColdTarget(int n) { + coldTarget += n; + if (coldTarget < minColdSize) { + coldTarget = minColdSize; + } else if (coldTarget > maxColdSize) { + coldTarget = maxColdSize; + } + } + + // Test period should be set as the largest recency of the hot entry. If an entry is + // older than the largest recency of the hot entry, the test period has been expired. + private boolean inTestPeriod(Node node) { + return sizeHot == 0 || node.epoch > headHot.prev.epoch; + } + + /** Prints out the internal state of the policy. */ + private void printClock() { + if (sizeCold > 0) { + System.out.println("** CLOCK-Pro list COLD HEAD (small recency) **"); + for (Node n = headCold.next; n != headCold; n = n.next) { + System.out.println(n.toString()); + } + System.out.println("** CLOCK-Pro list COLD TAIL (large recency) **"); + } + if (sizeHot > 0) { + System.out.println("** CLOCK-Pro list HOT HEAD (small recency) **"); + for (Node n = headHot.next; n != headHot; n = n.next) { + System.out.println(n.toString()); + } + System.out.println("** CLOCK-Pro list HOT TAIL (large recency) **"); + } + if (sizeNR > 0) { + System.out.println("** CLOCK-Pro list NR HEAD (small recency) **"); + for (Node n = headNonResident.next; n != headNonResident; n = n.next) { + System.out.println(n.toString()); + } + System.out.println("** CLOCK-Pro list NR TAIL (large recency) **"); + } + } + + enum Status { + HOT, COLD, NR, + } + + static final class Node { + final long key; + long epoch; + + Status status; + Node prev; + Node next; + + boolean marked; + + public Node() { + this.key = Long.MIN_VALUE; + prev = next = this; + } + + public Node(long key, long epoch) { + this.key = key; + prev = next = this; + this.epoch = epoch; + this.status = Status.COLD; + } + + public void unlink() { + prev.next = next; + next.prev = prev; + prev = next = this; + } + + public void link(Node node) { + prev = node; + next = node.next; + prev.next = this; + next.prev = this; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("key", key) + .add("marked", marked) + .add("type", status) + .add("epoch", epoch) + .toString(); + } + } +} diff --git a/simulator/src/main/resources/reference.conf b/simulator/src/main/resources/reference.conf index 6d624304be..12bae30646 100644 --- a/simulator/src/main/resources/reference.conf +++ b/simulator/src/main/resources/reference.conf @@ -92,6 +92,7 @@ caffeine.simulator { irr.DClock, irr.ClockPro, irr.ClockProPlus, + irr.ClockProSimple, # Policies based on the FRD algorithm irr.Frd,