/* * 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 static com.google.common.base.Preconditions.checkState; import static java.util.Objects.requireNonNull; import java.util.Set; import com.github.benmanes.caffeine.cache.simulator.BasicSettings; import com.github.benmanes.caffeine.cache.simulator.policy.Policy; import com.github.benmanes.caffeine.cache.simulator.policy.PolicyStats; import com.google.common.collect.ImmutableSet; import com.typesafe.config.Config; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; /** * 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. * <p> * 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 * <a href="http://www.ece.eng.wayne.edu/~sjiang/pubs/papers/jiang05_CLOCK-Pro.pdf">CLOCK-Pro: An * Effective Improvement of the CLOCK Replacement</a> and * <a href="http://www.slideshare.net/huliang64/clockpro">Clock-Pro: An Effective Replacement in OS * Kernel</a>. * <p> * This implementation is based on * <a href="https://bitbucket.org/SamiLehtinen/pyclockpro">PyClockPro</a> by Sami Lehtinen, * available under the MIT license. * * @author ben.manes@gmail.com (Ben Manes) */ public final class ClockProPolicy implements Policy { private final Long2ObjectMap<Node> data; private final PolicyStats policyStats; // Points to the hot page with the largest recency. The position of this hand actually serves as a // threshold of being a hot page. Any hot pages swept by the hand turn into cold ones. For the // convenience of the presentation, we call the page pointed to by HAND as the tail of the list, // and the page immediately after the tail page in the clockwise direction as the head of the // list. private Node handHot; // Points to the last resident cold page (i.e., the farthest one to the list head). Because we // always select this cold page for replacement, this is the position where we start to look for a // victim page, equivalent to the hand in CLOCK. private Node handCold; // Points to the last cold page in the test period. This hand is used to terminate the test period // of cold pages. The non-resident cold pages swept over by this hand will leave the circular // list. private Node handTest; // Maximum number or resident pages (cold + hot) private final int maximumSize; // Maximum number of cold pages (adaptive): // - increases when test page gets a hit // - decreases when test page is removed private int maximumColdSize; private int sizeHot; private int sizeCold; private int sizeTest; public ClockProPolicy(Config config) { BasicSettings settings = new BasicSettings(config); policyStats = new PolicyStats("irr.ClockPro"); maximumColdSize = settings.maximumSize(); data = new Long2ObjectOpenHashMap<>(); maximumSize = settings.maximumSize(); // All the hands move in the clockwise direction handHot = handCold = handTest = null; } /** Returns all variations of this policy based on the configuration parameters. */ public static Set<Policy> policies(Config config) { return ImmutableSet.of(new ClockProPolicy(config)); } @Override public void record(long key) { Node node = data.get(key); if (node == null) { onMiss(key); } else if (node.status == Status.TEST) { onNonResidentHit(node); } else { onHit(node); } } private void onMiss(long key) { policyStats.recordOperation(); policyStats.recordMiss(); Node node = new Node(key); data.put(key, node); add(node); sizeCold++; checkState((sizeHot + sizeCold) <= maximumSize); } private void onNonResidentHit(Node node) { policyStats.recordOperation(); policyStats.recordMiss(); if (maximumColdSize < maximumSize) { maximumColdSize++; } delete(node); sizeTest--; checkState(sizeTest >= 0); node.status = Status.HOT; add(node); sizeHot++; checkState((sizeHot + sizeCold) <= maximumSize); } private void onHit(Node node) { policyStats.recordOperation(); policyStats.recordHit(); node.marked = true; } /** Add meta data after hand hot, evict data if required, and update hands accordingly. */ private void add(Node node) { evict(); if (handHot == null) { handHot = handCold = handTest = node; node.next = node.prev = node; } else { node.prev = handHot; node.next = handHot.next; handHot.next.prev = node; handHot.next = node; } if (handCold == handHot) { handCold = node.next; } if (handTest == handHot) { handTest = node.next; } handHot = node.next; } /** Delete meta data data, update hands accordingly. */ private void delete(Node node) { if (handHot == node) { handHot = node.next; } if (handCold == node) { handCold = node.next; } if (handTest == node) { handTest = node.next; } node.remove(); } /** Evict pages from cache using the cold hand. */ private void evict() { // Now let us summarize how these hands coordinate their operations on the clock to resolve a // page fault. When there is a page fault, the faulted page must be a cold page. We first run // handCold for a free space. If the faulted cold page is not in the list, its reuse distance is // highly likely to be larger than the recency of hot pages. So the page is still categorized // as a cold page and is placed at the list head. The page also initiates its test period. If // the number of cold pages is larger than the threshold (maxCold + max), we run handTest. If // the cold page is in the list , the faulted page turns into a hot page and is placed at the // head of the list. We run handHot to turn a hot page with a large recency into a cold page. while (maximumSize <= (sizeHot + sizeCold)) { policyStats.recordOperation(); scanCold(); } } /** Moves the hot hand forward. */ private void scanHot() { // As mentioned above, what triggers the movement of handHot is that a cold page is found to // have been accessed in its test period and thus turns into a hot page, which maybe accordingly // turns the hot page with the largest recency into a cold page. If the reference bit of the hot // page pointed to by handHot is unset, we can simply change its status and then move the hand // forward. However, if the bit is set, which indicates the page has been re-accessed, we // spare this page, reset its reference bit and keep it as a hot page. This is because the // actual access time of the hot page could be earlier than the cold page. Then we move the hand // forward and do the same on the hot pages with their bits set until the hand encounters a hot // page with a reference bit of zero. Then the hot page turns into a cold page. Note that moving // handHot forward is equivalent to leaving the page it moves by at the list head. Whenever // the hand encounters a cold page, it will terminate the page’s test period. The hand will also // remove the cold page from the clock if it is non-resident (the most probable case). It // actually does the work on the cold page on behalf of hand handTest. Finally the hand stops at // a hot page. if (handHot == handTest) { scanTest(); } if (handHot.status == Status.HOT) { if (handHot.marked) { handHot.marked = false; } else { handHot.status = Status.COLD; sizeCold++; sizeHot--; } } // Move the hand forward handHot = handHot.next; } private void scanCold() { // The handCold is used to search for a resident cold page for replacement. If the reference // bit of the cold page currently pointed to by the handCold is unset, we replace the cold page // for a free space. The replaced cold page will remain in the list as a non-resident cold page // until it runs out of its test period, if it is in its test period. If not, we move it out of // the clock. However, if its bit is set and it is in its test period, we turn the cold page // into a hot page, and ask handHot for its actions, because an access during the test period // indicates a competitively small reuse distance. If its bit is set but it is not in its test // period, there are no status change as well as handHot actions. In both of the cases, its // reference bit is reset, and we move it to the list head. The hand will keep moving until it // encounters a cold page eligible for replacement, and stops at the next resident cold page. if (handCold.status == Status.COLD) { if (handCold.marked) { handCold.status = Status.HOT; handCold.marked = false; sizeCold--; sizeHot++; } else { policyStats.recordEviction(); handCold.status = Status.TEST; sizeCold--; sizeTest++; while (maximumSize < sizeTest) { policyStats.recordOperation(); scanTest(); } } checkState(sizeCold >= 0); } // Move the hand forward handCold = handCold.next; while ((maximumSize - maximumColdSize) < sizeHot) { policyStats.recordOperation(); scanHot(); } } private void scanTest() { // We keep track of the number of non-resident cold pages. Once the number exceeds the memory // size in the number of pages, we terminate the test period of the cold page pointed to by // handTest. We also remove it from the clock if it is a non-resident page. Because the cold // page has used up its test period without a re-access and has no chance to turn into a hot // page with its next access. handTest then moves forward and stops at the next cold page. if (handTest == handCold) { scanCold(); } if (handTest.status == Status.TEST) { requireNonNull(data.remove(handTest.key)); delete(handTest); sizeTest--; if (maximumColdSize > 1) { maximumColdSize--; } } // Move the hand forward handTest = handTest.next; } @Override public PolicyStats stats() { return policyStats; } @Override public void finished() { checkState(sizeHot + sizeCold + sizeTest == data.size()); checkState(sizeHot + sizeCold <= maximumSize); checkState(maximumColdSize <= maximumSize); } enum Status { HOT, COLD, TEST, } private static final class Node { final long key; boolean marked; Status status; Node prev; Node next; public Node(long key) { status = Status.COLD; this.key = key; } /** Removes the node from the list. */ public void remove() { prev.next = next; next.prev = prev; prev = next = null; } } }