/*
* 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;
}
}
}