/*
* 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 + "]";
}
}
}