/* * Copyright 2010 Google Inc. 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.googlecode.concurrentlinkedhashmap.caches; import java.util.AbstractMap; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; /** * A {@link java.util.Map} with a bounded size. If the cache is full when a new * entry is added, an old entry will first be evicted. * * <p>Eviction is accomplished using the <a * href="http://www.cse.ohio-state.edu/hpcs/WWW/HTML/publications/abs02-6.html"> * LIRS</a> algorithm, which is superior to LRU as it combines frequency with * recency in selecting elements to evict. There is also a succinct summary of * LIRS in the <a href="http://lists.mysql.com/commits/28601">MySQL * documentation</a>. * * <p>While LRU is a common cache eviction algorithm, it has long been faulted * for its focus on recency at the expense of frequency. It also suffers from * not being scan-resistant. Numerous algorithms have been proposed to improve * upon LRU, inevitably by combining some notion frequency with recency when * selecting eviction candidates, such as LIRS, ARC, FBR, LRFU, LRU-2, 2Q, and * MQ. We have selected LIRS as its overhead is very similar to that of LRU, * while outperforming LRU and many other similar algorithms. It has also been * adopted by MySQL, NetBSD, and Linux (as reported in an <a * href="http://engineering.osu.edu/news/?p=544">OSU news article</a>). * * <p>Section 1.2 of the LIRS paper provides an executive summary of the policy: * * <blockquote> * "We use recent Inter-Reference Recency (IRR) as the recorded history * information of each block, where IRR of a block refers to the number of * other blocks accessed between two consecutive references to the block. * Specifically, the recency refers to the number of other blocks accessed * from last reference to the current time... We assume that if the IRR of a * block is large, the next IRR of the block is likely to be large again. * Following this assumption, we select the blocks with large IRRs for * replacement, because these blocks are highly possible to be evicted later * by LRU before being referenced again under our assumption. It is noted * that these evicted blocks may also have been recently accessed, i.e. each * has a small recency." * </blockquote> * * @author fry@google.com (Charles Fry) */ @SuppressWarnings("unused") public class LirsMap<K, V> extends AbstractMap<K, V> { /** * The status of a cache entry. */ private enum Status { HOT, // resident LIRS, in stack, never in queue COLD, // resident HIRS, always in queue, sometimes in stack NONRES // non-resident HIRS, may be in stack, never in queue } /** * The percentage of the cache which is dedicated to hot blocks. * See section 5.1 */ private static final float HOT_RATE = 0.99f; /** * The backing map in which {@code LirsEntry}s are stored. It contains all * hot and cold entries, as well as all non-resident entries which are on * the stack. * * <p>Cache removals should not directly remove entries from this map, but * rather call {@link LirsEntry#remove()}, which will change the entry's * status to non-resident, while maintaining its recency. */ private final ConcurrentMap<K, LirsEntry> backingMap; // LIRS fields /** * This header encompasses two data structures: * * <ul> * <li>The LIRS stack, S, which is maintains recency information. All hot * entries are on the stack. All cold and non-resident entries which are more * recent than the least recent hot entry are also stored in the stack (the * stack is always pruned such that the last entry is hot, and all entries * accessed more recently than the last hot entry are present in the stack). * The stack is ordered by recency, with its most recently accessed entry * at the top, and its least recently accessed entry at the bottom.</li> * * <li>The LIRS queue, Q, which enqueues all cold entries for eviction. Cold * entries (by definition in the queue) may be absent from the stack (due to * pruning of the stack). Cold entries are added to the end of the queue * and entries are evicted from the front of the queue.</li> * </ul> */ private final LirsEntry header = new LirsEntry(); /** The maximum number of hot entries (L_lirs in the paper). */ private final int maximumHotSize; /** The maximum number of resident entries (L in the paper). */ private final int maximumSize; /** The actual number of hot entries. */ private int hotSize = 0; /** The actual number of resident entries. */ private int size = 0; /** * Constructs a new {@code BoundedCache} with a maximum size of {@code * maximumSize}. The maximum size is maintained by evicting old entries * prior to the adding new entries when the cache is full. * * @param maximumSize the maximum number of entries to store in the map */ public LirsMap(int maximumSize) { this.maximumSize = maximumSize; this.maximumHotSize = calculateMaxHotSize(maximumSize); this.backingMap = new ConcurrentHashMap<K, LirsEntry>(maximumSize); } /** * Returns the maximum hot size as a function of the maximum size. This is * defined to be the a percentage of the maximum size. When the maximum hot * size is equal to the maximum size the eviction algorithm reduces to LRU; * to avoid this we decrease the maximum hot size by one when it equals the * maximum size (and the maximum size is greater than one). */ private static int calculateMaxHotSize(int maximumSize) { int result = (int) (HOT_RATE * maximumSize); return (result == maximumSize) ? maximumSize - 1 : result; } @Override public V get(Object key) { LirsEntry e = backingMap.get(key); if (e == null) { return null; } if (e.isResident()) { e.hit(); } else { e.miss(); } return e.getValue(); } @Override public V put(K key, V value) { LirsEntry e = new LirsEntry(key, value); LirsEntry previous = backingMap.put(key, e); if (previous != null) { previous.remove(); return previous.value; } return null; } @Override public V remove(Object key) { // don't remove from the map here, as that would discard its recency LirsEntry e = backingMap.get(key); return (e == null) ? null : e.remove(); } /** * Returns the value associated with a key <i>without</i> accessing the key. * This allows test cases to observe the state of the cache without accessing * the cache (and changing the hot/cold status of entries). */ V lookupElement(K key) { LirsEntry e = backingMap.get(key); if (e != null && e.isResident()) { return e.getValue(); } return null; } @Override public int size() { return size; } @Override public Set<Entry<K, V>> entrySet() { throw new UnsupportedOperationException(); } /** * Prunes HIR blocks in the bottom of the stack until an HOT block sits in * the stack bottom. If pruned blocks were resident, then they * remain in the queue; otherwise they are no longer referenced, and are thus * removed from the backing map. */ private void pruneStack() { // See section 3.3: // "We define an operation called "stack pruning" on the LIRS // stack S, which removes the HIR blocks in the bottom of // the stack until an LIR block sits in the stack bottom. This // operation serves for two purposes: (1) We ensure the block in // the bottom of the stack always belongs to the LIR block set. // (2) After the LIR block in the bottom is removed, those HIR // blocks contiguously located above it will not have chances to // change their status from HIR to LIR, because their recencies // are larger than the new maximum recency of LIR blocks." LirsEntry bottom = stackBottom(); while (bottom != null && bottom.status != Status.HOT) { bottom.removeFromStack(); if (bottom.status == Status.NONRES) { // map only needs to hold nonresident entries that are on the stack backingMap.remove(bottom); } bottom = stackBottom(); } } /** * Returns the entry at the top of the stack. */ private LirsEntry stackTop() { LirsEntry top = header.nextInStack; return (top == header) ? null : top; } /** * Returns the entry at the bottom of the stack. */ private LirsEntry stackBottom() { LirsEntry bottom = header.previousInStack; return (bottom == header) ? null : bottom; } /** * Returns the entry at the front of the queue. */ private LirsEntry queueFront() { LirsEntry front = header.nextInQueue; return (front == header) ? null : front; } /** * Returns the entry at the end of the queue. */ private LirsEntry queueEnd() { LirsEntry end = header.previousInQueue; return (end == header) ? null : end; } /** * Returns a string representation of the stack. Useful for debugging. */ public String printStack() { StringBuilder result = new StringBuilder(); result.append("["); LirsEntry e = stackTop(); if (e != null) { result.append(e); for (e = e.nextInStack; e != header; e = e.nextInStack) { result.append(", " + e); } } result.append("]"); return result.toString(); } /** * Returns a string representation of the stack. Useful for debugging. */ public String printQueue() { StringBuilder result = new StringBuilder(); result.append("["); LirsEntry e = queueFront(); if (e != null) { result.append(e); for (e = e.nextInQueue; e != header; e = e.nextInQueue) { result.append(", " + e); } } result.append("]"); return result.toString(); } /** * Wraps a key with pointers into the LIRS stack and queue. */ private class LirsEntry { // The underlying entry key and value. private final K key; private V value; /** * The status of this entry (hot/cold/non-resident). This should never be * changed manually, but rather using the methods {@link #hot()}, * {@link #cold()}, and {@link #nonResident()} in order to ensure that * proper book-keeping is done of the cache size and the hot entry count. */ private Status status = Status.NONRES; // LIRS stack S private LirsEntry previousInStack; private LirsEntry nextInStack; // LIRS queue Q private LirsEntry previousInQueue; private LirsEntry nextInQueue; // Note that the queue could be implemented as a separate data structure. // This would remove these two references from each entry, but it would // require the queue to be traversed when looking for elements in the queue. /** * Constructs a new entry for a given key and value, adding it to the * appropriate LIRS data structures. */ public LirsEntry(K key, V value) { this.key = key; this.value = value; miss(); } /** * Creates a header entry. */ public LirsEntry() { this.key = null; this.value = null; // initially point everything back to self this.previousInStack = this; this.nextInStack = this; this.previousInQueue = this; this.nextInQueue = this; } /** * Returns this entry's value. */ public V getValue() { return value; } public void setValue(V value) { this.value = value; } /** * Returns true if this entry is resident in the cache, false otherwise. */ public boolean isResident() { return (status != Status.NONRES); } /** * Returns true if this entry is in the stack, false otherwise. */ public boolean inStack() { return (nextInStack != null); } /** * Returns true if this entry is in the queue, false otherwise. */ public boolean inQueue() { return (nextInQueue != null); } /** * Records a cache hit. */ public void hit() { switch (status) { case HOT: hotHit(); break; case COLD: coldHit(); break; case NONRES: throw new IllegalStateException("Can't hit a non-resident entry!"); default: throw new AssertionError("Hit with unknown status: " + status); } } /** * Records a cache hit on a hot block. */ private void hotHit() { // See section 3.3 case 1: // "Upon accessing an LIR block X: // This access is guaranteed to be a hit in the cache." // "We move it to the top of stack S." boolean onBottom = (stackBottom() == this); moveToStackTop(); // "If the LIR block is originally located in the bottom of the stack, // we conduct a stack pruning." if (onBottom) { pruneStack(); } } /** * Records a cache hit on a cold block. */ private void coldHit() { // See section 3.3 case 2: // "Upon accessing an HIR resident block X: // This is a hit in the cache." // "We move it to the top of stack S." boolean inStack = inStack(); moveToStackTop(); // "There are two cases for block X:" if (inStack) { // "(1) If X is in the stack S, we change its status to LIR." hot(); // "This block is also removed from list Q." removeFromQueue(); // "The LIR block in the bottom of S is moved to the end of list Q // with its status changed to HIR." stackBottom().migrateToQueue(); // "A stack pruning is then conducted." pruneStack(); } else { // "(2) If X is not in stack S, we leave its status in HIR and move // it to the end of list Q." moveToQueueEnd(); } } /** * Records a cache miss. This is how new entries join the LIRS stack and * queue. This is called both when a new entry is first created, and when a * non-resident entry is re-computed. */ private void miss() { if (hotSize < maximumHotSize) { warmupMiss(); } else { fullMiss(); } // now the missed item is in the cache size++; } /** * Records a miss when the hot entry set is not full. */ private void warmupMiss() { // See section 3.3: // "When LIR block set is not full, all the referenced blocks are // given an LIR status until its size reaches L_lirs." hot(); moveToStackTop(); } /** * Records a miss when the hot entry set is full. */ private void fullMiss() { // See section 3.3 case 3: // "Upon accessing an HIR non-resident block X: // This is a miss." // This condition is unspecified in the paper, but appears to be // necessary. if (size >= maximumSize) { // "We remove the HIR resident block at the front of list Q (it then // becomes a non-resident block), and replace it out of the cache." queueFront().evict(); } // "Then we load the requested block X into the freed buffer and place // it on the top of stack S." boolean inStack = inStack(); moveToStackTop(); // "There are two cases for block X:" if (inStack) { // "(1) If X is in stack S, we change its status to LIR and move the // LIR block in the bottom of stack S to the end of list Q with its // status changed to HIR. A stack pruning is then conducted. hot(); stackBottom().migrateToQueue(); pruneStack(); } else { // "(2) If X is not in stack S, we leave its status in HIR and place // it in the end of list Q." cold(); } } /** * Marks this entry as hot. */ private void hot() { if (status != Status.HOT) { hotSize++; } status = Status.HOT; } /** * Marks this entry as cold. */ private void cold() { if (status == Status.HOT) { hotSize--; } status = Status.COLD; moveToQueueEnd(); } /** * Marks this entry as non-resident. */ @SuppressWarnings("fallthrough") private void nonResident() { switch (status) { case HOT: hotSize--; // fallthrough case COLD: size--; default: } status = Status.NONRES; } /** * Temporarily removes this entry from the stack, fixing up neighbor links. * This entry's links remain unchanged, meaning that {@link #inStack()} will * continue to return true. This should only be called if this node's links * will be subsequently changed. */ private void tempRemoveFromStack() { if (inStack()) { previousInStack.nextInStack = nextInStack; nextInStack.previousInStack = previousInStack; } } /** * Removes this entry from the stack. */ private void removeFromStack() { tempRemoveFromStack(); previousInStack = null; nextInStack = null; } /** * Inserts this entry before the specified existing entry in the stack. */ private void addToStackBefore(LirsEntry existingEntry) { previousInStack = existingEntry.previousInStack; nextInStack = existingEntry; previousInStack.nextInStack = this; nextInStack.previousInStack = this; } /** * Moves this entry to the top of the stack. */ private void moveToStackTop() { tempRemoveFromStack(); addToStackBefore(header.nextInStack); } /** * Moves this entry to the bottom of the stack. */ private void moveToStackBottom() { tempRemoveFromStack(); addToStackBefore(header); } /** * Temporarily removes this entry from the queue, fixing up neighbor links. * This entry's links remain unchanged. This should only be called if this * node's links will be subsequently changed. */ private void tempRemoveFromQueue() { if (inQueue()) { previousInQueue.nextInQueue = nextInQueue; nextInQueue.previousInQueue = previousInQueue; } } /** * Removes this entry from the queue. */ private void removeFromQueue() { tempRemoveFromQueue(); previousInQueue = null; nextInQueue = null; } /** * Inserts this entry before the specified existing entry in the queue. */ private void addToQueueBefore(LirsEntry existingEntry) { previousInQueue = existingEntry.previousInQueue; nextInQueue = existingEntry; previousInQueue.nextInQueue = this; nextInQueue.previousInQueue = this; } /** * Moves this entry to the end of the queue. */ private void moveToQueueEnd() { tempRemoveFromQueue(); addToQueueBefore(header); } /** * Moves this entry from the stack to the queue, marking it cold * (as hot entries must remain in the stack). This should only be called * on resident entries, as non-resident entries should not be made resident. * The bottom entry on the queue is always hot due to stack pruning. */ private void migrateToQueue() { removeFromStack(); cold(); } /** * Moves this entry from the queue to the stack, marking it hot (as cold * resident entries must remain in the queue). */ private void migrateToStack() { removeFromQueue(); if (!inStack()) { moveToStackBottom(); } hot(); } /** * Evicts this entry, removing it from the queue and setting its status to * cold non-resident. If the entry is already absent from the stack, it is * removed from the backing map; otherwise it remains in order for its * recency to be maintained. */ private void evict() { removeFromQueue(); // without computation nonresident entries are never revived removeFromStack(); backingMap.remove(key, this); nonResident(); value = null; } /** * Removes this entry from the cache. This operation is not specified in * the paper, which does not account for forced eviction. */ private V remove() { boolean wasHot = (status == Status.HOT); V result = value; evict(); // attempt to maintain a constant number of hot entries if (wasHot) { LirsEntry end = queueEnd(); if (end != null) { end.migrateToStack(); } } return result; } @Override public String toString() { return key + "=" + value + " [" + status + "]"; } } }