/*
* 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.adaptive;
import static com.google.common.base.Preconditions.checkState;
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.base.MoreObjects;
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;
/**
* CAR with Temporal filtering policy. This algorithm differs from CAR by maintaining a temporal
* locality window such that pages that are re-requested within the window are of short-term utility
* and pages that are re-requested outside the window are of long-term utility. The temporal
* locality window is an adaptable parameter of the algorithm.
* <p>
* This implementation is based on the pseudo code provided by the authors in their paper <a href=
* "https://www.usenix.org/legacy/publications/library/proceedings/fast04/tech/full_papers/bansal/bansal.pdf">
* CAR: Clock with Adaptive Replacement</a> and is further described in their paper,
* <p>
* This algorithm is patented by IBM (6996676, 7096321, 7058766, 8612689).
*
* @author ben.manes@gmail.com (Ben Manes)
*/
public final class CartPolicy implements Policy {
private final Long2ObjectMap<Node> data;
private final PolicyStats policyStats;
private final int maximumSize;
private final Node headT1;
private final Node headT2;
private final Node headB1;
private final Node headB2;
private int sizeT1;
private int sizeT2;
private int sizeB1;
private int sizeB2;
private int sizeS;
private int sizeL;
private int p;
private int q;
public CartPolicy(Config config) {
BasicSettings settings = new BasicSettings(config);
this.policyStats = new PolicyStats("adaptive.Cart");
this.data = new Long2ObjectOpenHashMap<>();
this.maximumSize = settings.maximumSize();
this.headT1 = new Node();
this.headT2 = new Node();
this.headB1 = new Node();
this.headB2 = new Node();
}
/** Returns all variations of this policy based on the configuration parameters. */
public static Set<Policy> policies(Config config) {
return ImmutableSet.of(new CartPolicy(config));
}
@Override
public void record(long key) {
Node node = data.get(key);
if (isHit(node)) {
policyStats.recordHit();
onHit(node);
} else {
policyStats.recordMiss();
onMiss(key, node);
}
}
private static boolean isHit(Node node) {
// if (x is in T1 ∪ T2) then cache hit
return (node != null) && ((node.type == QueueType.T1) || (node.type == QueueType.T2));
}
private void onHit(Node node) {
// Set the page reference bit for x
node.marked = true;
policyStats.recordOperation();
}
private void onMiss(long key, Node node) {
// if (|T1| + |T2| = c) then
// /* cache full, replace a page from cache */
// replace()
// /* history replacement */
// if ((x ̸∈ B1 ∪ B2) and (|B1| + |B2| = c + 1) and
// ((|B1| > max{0, q}) or (B2 is empty))) then
// Remove the bottom page in B1 from the history.
// else if ((x ̸∈ B1 ∪ B2) and (|B1| + |B2| = c + 1)) then
// Remove the bottom page in B2 from the history.
//
// /* history miss */
// if (x is not in B1 ∪ B2) then
// Insert x at the tail of T1.
// Reset the page reference bit of x
// Set filter bit of x to "S"
// nS = nS + 1 /* history hit */
// else if (x is in B1) then
// Adapt: Increase the target size for the list T1 as: p = min{p + max{1, nS / |B1|}, c}
// Move x to the tail of T1
// Reset the page reference bit of x
// Set nL = nL + 1.
// Set type of x to "L".
// /* history hit */
// else /* x must be in B2 */
// Adapt: Decrease the target size for the list T1 as: p = max{p − max{1, nL / |B2|}, 0}
// Move x to the tail of T1
// Reset the page reference bit of x
// Set nL = nL + 1
// if (|T2|+|B2|+|T1|−nS ≥ c) then
// Set target q = min(q + 1, 2c − |T1|)
policyStats.recordOperation();
if ((sizeT1 + sizeT2) == maximumSize) {
demote();
if (!isGhost(node) && ((sizeB1 + sizeB2) == (maximumSize + 1))) {
if (sizeB1 > Math.max(0, q) || (sizeB2 == 0)) {
// Remove the bottom page in B1 from the history
Node victim = headB1.next;
data.remove(victim.key);
victim.remove();
sizeB1--;
} else {
// Remove the bottom page in B2 from the history
Node victim = headB2.next;
data.remove(victim.key);
victim.remove();
sizeB2--;
}
}
}
if (!isGhost(node)) {
// Insert x at the tail of T1
checkState(node == null);
node = new Node(key);
node.appendToTail(headT1);
node.type = QueueType.T1;
data.put(key, node);
sizeT1++;
// Set filter bit of x to "S"
// nS = nS + 1 /* history hit */
node.filter = FilterType.ShortTerm;
sizeS++;
} else if (node.type == QueueType.B1) {
// Adapt: Increase the target size for the list T1 as: p = min{p + max{1, nS / |B1|}, c}
p = Math.min(p + Math.max(1, sizeS / sizeB1), maximumSize);
// Move x to the tail of T1
node.remove();
sizeB1--;
node.appendToTail(headT1);
node.type = QueueType.T1;
sizeT1++;
// Reset the page reference bit of x
// Set nL = nL + 1.
// Set type of x to "L".
node.filter = FilterType.LongTerm;
node.marked = false;
sizeL++;
} else if (node.type == QueueType.B2) {
// Adapt: Decrease the target size for the list T1 as: p = max{p − max{1, nL / |B2|}, 0}
p = Math.max(p - Math.max(1, sizeL / sizeB2), 0);
// Move x to the tail of T1
node.remove();
sizeB2--;
node.appendToTail(headT1);
node.type = QueueType.T1;
sizeT1++;
// Reset the page reference bit of x
// Set nL = nL + 1
node.filter = FilterType.LongTerm;
node.marked = false;
sizeL++;
// if (|T2|+|B2|+|T1|−nS ≥ c) then
// Set target q = min(q + 1, 2c − |T1|)
if (sizeT2 + sizeB2 + sizeT1 - sizeS >= maximumSize) {
q = Math.min(q + 1, (2 * maximumSize) - sizeT1);
}
} else {
throw new IllegalStateException();
}
}
private static boolean isGhost(Node node) {
return (node != null) && ((node.type == QueueType.B1) || (node.type == QueueType.B2));
}
private void demote() {
// while (the page reference bit of the head page in T2 is 1)) then
// Move the head page in T2 to tail position in T1
// Reset the page reference bit
// if (|T2|+|B2|+|T1|−nS ≥ c) then
// Set target q = min(q + 1, 2c − |T1|)
//
// /* The following while loop should stop, if T1 is empty */
// while ((the filter bit of the head page in T1 is "L")
// or (the page reference bit of the head page in T1 is set))
// if ((the page reference bit of the head page in T1 is set)
// Move the head page in T1 to tail position in T1
// Reset the page reference bit
// if ((|T1| ≥ min(p + 1, |B1|)) and (the filter bit of the moved page is "S")) then
// set type of x to "L"
// nS = nS − 1
// nL = nL + 1
// else
// Move the head page in T1 to tail position in T2
// Reset the page reference bit
// Set q = max(q − 1, c − |T1|)
//
// if (|T1| >= max(1, p)) then
// Demote the head page in T1 and make it the MRU page in B1
// nS = nS − 1
// else
// Demote the head page in T2 and make it the MRU page in B2
// nL = nL − 1
policyStats.recordEviction();
while (headT2.next.marked) {
policyStats.recordOperation();
Node demoted = headT2.next;
demoted.marked = false;
demoted.remove();
sizeT2--;
demoted.appendToTail(headT1);
demoted.type = QueueType.T1;
sizeT1++;
if (sizeT2 + sizeB2 + sizeT1 - sizeS >= maximumSize) {
q = Math.min(q + 1, (2 * maximumSize) - sizeT1);
}
}
while ((headT1.next.filter == FilterType.LongTerm) || headT1.next.marked) {
policyStats.recordOperation();
Node node = headT1.next;
if (node.marked) {
node.moveToTail(headT1);
node.marked = false;
if ((sizeT1 >= Math.max(p + 1, sizeB1)) && (node.filter == FilterType.ShortTerm)) {
node.filter = FilterType.LongTerm;
sizeS--;
sizeL++;
}
} else {
node.remove();
node.type = QueueType.T2;
node.appendToTail(headT2);
sizeT1--;
sizeT2++;
q = Math.max(q - 1, maximumSize - sizeT1);
}
}
if (sizeT1 >= Math.max(1, p)) {
Node demoted = headT1.next;
demoted.remove();
demoted.type = QueueType.B1;
demoted.appendToTail(headB1);
sizeT1--;
sizeB1++;
sizeS--;
} else {
Node demoted = headT2.next;
demoted.remove();
demoted.type = QueueType.B2;
demoted.appendToTail(headB2);
sizeT2--;
sizeB2++;
sizeL--;
}
}
@Override
public PolicyStats stats() {
return policyStats;
}
@Override
public void finished() {
checkState(sizeT1 == data.values().stream().filter(node -> node.type == QueueType.T1).count());
checkState(sizeT2 == data.values().stream().filter(node -> node.type == QueueType.T2).count());
checkState(sizeB1 == data.values().stream().filter(node -> node.type == QueueType.B1).count());
checkState(sizeB2 == data.values().stream().filter(node -> node.type == QueueType.B2).count());
checkState((sizeT1 + sizeT2) <= maximumSize);
checkState((sizeB1 + sizeB2) <= maximumSize);
}
private enum QueueType {
T1, B1,
T2, B2,
}
private enum FilterType {
ShortTerm,
LongTerm,
}
static final class Node {
final long key;
Node prev;
Node next;
QueueType type;
FilterType filter;
boolean marked;
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) {
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) {
// 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(key != Long.MIN_VALUE);
prev.next = next;
next.prev = prev;
prev = next = null;
type = null;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("key", key)
.add("type", type)
.toString();
}
}
}