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