/*
* 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;
/**
* Clock with Adaptive Replacement policy. This algorithm differs from ARC by replacing the LRU
* policy with the 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(2n) eviction as the queues are scanned.
* <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 CarPolicy 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 p;
public CarPolicy(Config config) {
BasicSettings settings = new BasicSettings(config);
this.policyStats = new PolicyStats("adaptive.Car");
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 CarPolicy(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()
// /* cache directory replacement */
// if ((x is not in B1 ∪ B2) and (|T1|+|B1| = c)) then
// Discard the LRU page in B1.
// else if ((x is not in B1 ∪ B2) and (|T1|+|T2|+|B1|+|B2| = 2c)) then
// Discard the LRU page in B2.
//
// /* cache directory miss */
// if (x is not in B1 ∪ B2) then
// Insert x at the tail of T1.
// Reset the page reference bit of x // cache directory hit
// else if (x is in B1) then
// Adapt: Increase the target size for the list T1 as: p = min{p + max{1, |B2|/|B1|}, c}
// Move x at the tail of T2.
// Reset the page reference bit of x. // cache directory hit
// else /* x must be in B2 */
// Adapt: Decrease the target size for the list T1 as: p = max{p − max{1, |B1|/|B2|}, 0}
// Move x at the tail of T2.
// Set the page reference bit of x.
policyStats.recordOperation();
if ((sizeT1 + sizeT2) == maximumSize) {
demote();
if (!isGhost(node)) {
if ((sizeT1 + sizeB1) == maximumSize) {
// Discard the LRU page in B1
Node victim = headB1.next;
data.remove(victim.key);
victim.remove();
sizeB1--;
} else if ((sizeT1 + sizeT2 + sizeB1 + sizeB2) == (2 * maximumSize)) {
// Discard the LRU page in B2
Node victim = headB2.next;
data.remove(victim.key);
victim.remove();
sizeB2--;
}
}
}
if (!isGhost(node)) {
// Insert x at the tail of T1
// Reset the page reference bit of x
checkState(node == null);
node = new Node(key);
node.appendToTail(headT1);
node.type = QueueType.T1;
data.put(key, node);
sizeT1++;
} else if (node.type == QueueType.B1) {
// Adapt: Increase the target size for the list T1 as: p = min{p + max{1, |B2|/|B1|}, c}
// Move x at the tail of T2
// Reset the page reference bit of x.
p = Math.min(p + Math.max(1, sizeB2 / sizeB1), maximumSize);
node.remove();
sizeB1--;
node.appendToTail(headT2);
node.type = QueueType.T2;
sizeT2++;
node.marked = false;
} else if (node.type == QueueType.B2) {
// Adapt: Decrease the target size for the list T1 as: p = max{p − max{1, |B1|/|B2|}, 0}
// Move x at the tail of T2.
// Reset the page reference bit of x.
p = Math.max(p - Math.max(1, sizeB1 / sizeB2), 0);
node.remove();
sizeB2--;
node.appendToTail(headT2);
node.type = QueueType.T2;
sizeT2++;
node.marked = false;
} else {
throw new IllegalStateException();
}
}
private static boolean isGhost(Node node) {
return (node != null) && ((node.type == QueueType.B1) || (node.type == QueueType.B2));
}
@SuppressWarnings("PMD.ConfusingTernary")
private void demote() {
// found = 0
// repeat
// if (|T1| >= max(1, p)) then
// if (the page reference bit of head page in T1 is 0) then
// found = 1;
// Demote the head page in T1 and make it the MRU page in B1.
// else
// Set the page reference bit of head page in T1 to 0, and make it the tail page in T2.
// else
// if (the page reference bit of head page in T2 is 0), then
// found = 1;
// Demote the head page in T2 and make it the MRU page in B2.
// else
// Set the page reference bit of head page in T2 to 0, and make it the tail page in T2.
// until (found)
policyStats.recordEviction();
for (;;) {
policyStats.recordOperation();
if (sizeT1 >= Math.max(1, p)) {
Node candidate = headT1.next;
if (!candidate.marked) {
candidate.remove();
sizeT1--;
candidate.appendToTail(headB1);
candidate.type = QueueType.B1;
sizeB1++;
return;
} else {
candidate.marked = false;
candidate.remove();
sizeT1--;
candidate.appendToTail(headT2);
candidate.type = QueueType.T2;
sizeT2++;
}
} else {
Node candidate = headT2.next;
if (!candidate.marked) {
candidate.remove();
sizeT2--;
candidate.appendToTail(headB2);
candidate.type = QueueType.B2;
sizeB2++;
return;
} else {
candidate.marked = false;
candidate.remove();
candidate.appendToTail(headT2);
candidate.type = QueueType.T2;
}
}
}
}
@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,
}
static final class Node {
final long key;
Node prev;
Node next;
QueueType type;
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;
}
/** 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();
}
}
}