/* * 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.linked; import static com.google.common.base.Preconditions.checkState; import static java.util.stream.Collectors.toSet; import java.util.Arrays; import java.util.Set; import com.github.benmanes.caffeine.cache.simulator.BasicSettings; import com.github.benmanes.caffeine.cache.simulator.admission.Admission; import com.github.benmanes.caffeine.cache.simulator.admission.Admittor; 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.typesafe.config.Config; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; /** * "Quadruply-segmented LRU. Four queues are maintained at levels 0 to 3. On a cache miss, the item * is inserted at the head of queue 0. On a cache hit, the item is moved to the head of the next * higher queue (items in queue 3 move to the head of queue 3). Each queue is allocated 1/4 of the * total cache size and items are evicted from the tail of a queue to the head of the next lower * queue to maintain the size invariants. Items evicted from queue 0 are evicted from the cache." * * For more details, see <a href="http://www.cs.cornell.edu/~qhuang/papers/sosp_fbanalysis.pdf"An * Analysis of Facebook Photo Caching</a>. * * @author ben.manes@gmail.com (Ben Manes) */ public final class S4LruPolicy implements Policy { private final Long2ObjectMap<Node> data; private final PolicyStats policyStats; private final Admittor admittor; private final int maximumSize; private final Node[] headQ; private final int[] sizeQ; private final int levels; public S4LruPolicy(Admission admission, Config config) { this.policyStats = new PolicyStats(admission.format("linked.S4Lru")); this.admittor = admission.from(config, policyStats); S4LruSettings settings = new S4LruSettings(config); this.data = new Long2ObjectOpenHashMap<>(); this.maximumSize = settings.maximumSize(); this.levels = settings.levels(); this.headQ = new Node[levels]; this.sizeQ = new int[levels]; Arrays.setAll(headQ, Node::sentinel); } /** Returns all variations of this policy based on the configuration parameters. */ public static Set<Policy> policies(Config config) { BasicSettings settings = new BasicSettings(config); return settings.admission().stream().map(admission -> new S4LruPolicy(admission, config) ).collect(toSet()); } @Override public void record(long key) { policyStats.recordOperation(); Node node = data.get(key); admittor.record(key); if (node == null) { onMiss(key); policyStats.recordMiss(); } else { onHit(node); policyStats.recordHit(); } } private void onHit(Node node) { node.remove(); sizeQ[node.level]--; if (node.level < (levels - 1)) { node.level++; } Node head = headQ[node.level]; node.appendToTail(head); sizeQ[node.level]++; adjust(); } private void onMiss(long key) { Node node = new Node(key); data.put(key, node); node.appendToTail(headQ[0]); sizeQ[0]++; adjust(); evict(node); } private void adjust() { int maxPerLevel = maximumSize / levels; for (int i = levels - 1; i > 0; i--) { if (sizeQ[i] > maxPerLevel) { Node demote = headQ[i].next; demote.remove(); sizeQ[i]--; demote.level = i - 1; sizeQ[demote.level]++; demote.appendToTail(headQ[demote.level]); } } } private void evict(Node candidate) { if (data.size() > maximumSize) { policyStats.recordEviction(); Node victim = headQ[0].next; boolean admit = admittor.admit(candidate.key, victim.key); if (admit) { evictEntry(victim); sizeQ[0]--; } else { evictEntry(candidate); sizeQ[candidate.level]--; } } } private void evictEntry(Node node) { data.remove(node.key); node.remove(); } @Override public void finished() { for (int i = 0; i < levels; i++) { int level = i; long count = data.values().stream().filter(node -> node.level == level).count(); checkState(count == sizeQ[i]); } } @Override public PolicyStats stats() { return policyStats; } static final class Node { final long key; Node prev; Node next; int level; Node(long key) { this.key = key; } static Node sentinel(int level) { Node node = new Node(Long.MIN_VALUE); node.level = level; node.prev = node; node.next = node; return node; } /** 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; } @Override public String toString() { return MoreObjects.toStringHelper(this) .add("key", key) .add("level", level) .toString(); } } static final class S4LruSettings extends BasicSettings { public S4LruSettings(Config config) { super(config); } public int levels() { return config().getInt("s4lru.levels"); } } }