/* * 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.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; /** * "Segmented LRU is based on the observation that objects with at least two accesses are much more * popular than those with only one access during a short interval. In Segmented LRU, cache space is * partitioned into two segments: probationary segment and protected segment. * <p> * New objects (with only one access) are first faulted into the probationary segment, whereas * objects with two or more accesses are kept in the protected segment. When a probationary object * gets one more reference, it will change to the protected segment. When the whole cache space * becomes full, the least recently used object in the probationary segment will first be replaced. * The protected segment is finite in size. When it gets full, the overflowed will be re-cached in * probationary segment. Since objects in protected segment have to go a longer way before being * evicted, popular object or an object with more accesses tends to be kept in cache for longer * time." from <a href=" * http://www.is.kyusan-u.ac.jp/~chengk/pub/papers/compsac00_A07-07.pdf">LRU-SP: A Size-Adjusted and * Popularity-Aware LRU Replacement Algorithm for Web Caching</a> * * @author ben.manes@gmail.com (Ben Manes) */ public final class SegmentedLruPolicy implements Policy { static final Node UNLINKED = new Node(); final Long2ObjectMap<Node> data; final PolicyStats policyStats; final Node headProtected; final Node headProbation; final Admittor admittor; final int maxProtected; final int maximumSize; int sizeProtected; public SegmentedLruPolicy(Admission admission, Config config) { this.policyStats = new PolicyStats(admission.format("linked.SegmentedLru")); this.admittor = admission.from(config, policyStats); SegmentedLruSettings settings = new SegmentedLruSettings(config); this.headProtected = new Node(); this.headProbation = new Node(); this.maximumSize = settings.maximumSize(); this.data = new Long2ObjectOpenHashMap<>(); this.maxProtected = (int) (maximumSize * settings.percentProtected()); } /** 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 SegmentedLruPolicy(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); } else { onHit(node); } } private void onHit(Node node) { if (node.type == QueueType.PROTECTED) { node.moveToTail(headProtected); } else { sizeProtected++; if (sizeProtected > maxProtected) { Node demote = headProtected.next; demote.remove(); demote.type = QueueType.PROBATION; demote.appendToTail(headProbation); sizeProtected--; } node.remove(); node.type = QueueType.PROTECTED; node.appendToTail(headProtected); } policyStats.recordHit(); } private void onMiss(long key) { Node node = new Node(key); data.put(key, node); policyStats.recordMiss(); node.appendToTail(headProbation); node.type = QueueType.PROBATION; evict(node); } private void evict(Node candidate) { if (data.size() > maximumSize) { Node victim = (maxProtected == 0) ? headProtected.next // degrade to LRU : headProbation.next; policyStats.recordEviction(); boolean admit = admittor.admit(candidate.key, victim.key); if (admit) { evictEntry(victim); } else { evictEntry(candidate); } } } private void evictEntry(Node node) { data.remove(node.key); node.remove(); } @Override public PolicyStats stats() { return policyStats; } enum QueueType { PROTECTED, PROBATION, } static final class Node { final long key; Node prev; Node next; QueueType type; Node() { this.key = Long.MIN_VALUE; this.prev = this; this.next = this; } Node(long key) { this.key = key; this.prev = UNLINKED; this.next = UNLINKED; } /** 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 = UNLINKED; // mark as unlinked } @Override public String toString() { return MoreObjects.toStringHelper(this) .add("key", key) .add("type", type) .toString(); } } static final class SegmentedLruSettings extends BasicSettings { public SegmentedLruSettings(Config config) { super(config); } public double percentProtected() { return config().getDouble("segmented-lru.percent-protected"); } } }