/*
* 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.irr;
import static com.google.common.base.Preconditions.checkState;
import java.util.ArrayList;
import java.util.List;
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;
/**
* Low Inter-reference Recency Set (LIRS) algorithm. This algorithm organizes blocks by their
* inter-reference recency (IRR) and groups entries as either having a low (LIR) or high (HIR)
* recency. A LIR entry is preferable to retain in the cache and evicted HIR entries may be retained
* as non-resident HIR entries. This allows a non-resident HIR entry to be promoted to a LIR entry
* shortly after a cache miss. The authors recommend sizing the maximum number of LIR blocks to 99%
* of the cache's total size (1% remaining for HIR blocks).
* <p>
* The authors do not provide a recommendation for setting the maximum number of non-resident HIR
* blocks. To avoid unbounded memory usage, these blocks are placed on a non-resident queue to allow
* immediate removal, when a non-resident size limit is reached, instead of searching the stack.
* <p>
* The algorithm is explained by the authors in
* <a href="http://web.cse.ohio-state.edu/hpcs/WWW/HTML/publications/papers/TR-02-6.pdf">LIRS: An
* Efficient Low Inter-reference Recency Set Replacement Policy to Improve Buffer Cache
* Performance</a> and
* <a href="http://web.cse.ohio-state.edu/hpcs/WWW/HTML/publications/papers/TR-05-11.pdf">Making LRU
* Friendly to Weak Locality Workloads: A Novel Replacement Algorithm to Improve Buffer Cache
* Performance</a>.
*
* @author ben.manes@gmail.com (Ben Manes)
*/
@SuppressWarnings("PMD.TooManyFields")
public final class LirsPolicy implements Policy {
final Long2ObjectMap<Node> data;
final PolicyStats policyStats;
final List<Object> evicted;
final Node headNR;
final Node headS;
final Node headQ;
final int maximumNonResidentSize;
final int maximumHotSize;
final int maximumSize;
int sizeS;
int sizeQ;
int sizeNR;
int sizeHot;
int residentSize;
// Enable to print out the internal state
static final boolean debug = false;
public LirsPolicy(Config config) {
LirsSettings settings = new LirsSettings(config);
this.maximumNonResidentSize = (int) (settings.maximumSize() * settings.nonResidentMultiplier());
this.maximumHotSize = (int) (settings.maximumSize() * settings.percentHot());
this.policyStats = new PolicyStats("irr.Lirs");
this.data = new Long2ObjectOpenHashMap<>();
this.maximumSize = settings.maximumSize();
this.evicted = new ArrayList<>();
this.headNR = new Node();
this.headS = new Node();
this.headQ = new Node();
}
/** Returns all variations of this policy based on the configuration parameters. */
public static Set<Policy> policies(Config config) {
return ImmutableSet.of(new LirsPolicy(config));
}
@Override
public void record(long key) {
policyStats.recordOperation();
Node node = data.get(key);
if (node == null) {
node = new Node(key);
data.put(key,node);
onNonResidentHir(node);
} else if (node.status == Status.LIR) {
onLir(node);
} else if (node.status == Status.HIR_RESIDENT) {
onResidentHir(node);
} else if (node.status == Status.HIR_NON_RESIDENT) {
node.removeFrom(StackType.NR);
onNonResidentHir(node);
} else {
throw new IllegalStateException();
}
}
private void onLir(Node node) {
// 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. If the LIR block is originally located at the bottom of the
// stack, we conduct a stack pruning. This case is illustrated in the transition from state
// (a) to state (b) in Fig. 2.
policyStats.recordHit();
boolean wasBottom = (headS.prevS == node);
node.moveToTop(StackType.S);
if (wasBottom) {
pruneStack();
}
}
private void onResidentHir(Node node) {
// Upon accessing an HIR resident block X. This is a hit in the cache. We move it to the top
// of the stack S. There are two cases for the original location of block X: a) If X is in
// stack S, we change its status to LIR. This block is also removed from stack Q. The LIR
// block at the bottom of S is moved to the top of stack Q with its status changed to HIR. A
// stack pruning is then conducted. This case is illustrated in the transition from state (a)
// to state (c) in Fig. 2. b) If X is not in stack S, we leave its status unchanged and move
// it to the top of stack Q.
policyStats.recordHit();
boolean isInStack = node.isInStack(StackType.S);
boolean isTop = node.isStackTop(StackType.S);
node.moveToTop(StackType.S);
if (isInStack && !isTop) {
sizeHot++;
node.status = Status.LIR;
node.removeFrom(StackType.Q);
Node bottom = headS.prevS;
sizeHot--;
bottom.status = Status.HIR_RESIDENT;
bottom.removeFrom(StackType.S);
bottom.moveToTop(StackType.Q);
pruneStack();
} else {
node.moveToTop(StackType.Q);
}
}
private void onNonResidentHir(Node node) {
// When an LIR block set is not full, all the accessed blocks are given LIR status until its
// size reaches Llirs. After that, HIR status is given to any blocks that are accessed for the
// first time and to blocks that have not been accessed for a long time so that currently they
// are not in stack S.
policyStats.recordMiss();
if (sizeHot < maximumHotSize) {
onLirWarmupMiss(node);
} else if (residentSize < maximumSize) {
onHirWarmupMiss(node);
} else {
onFullMiss(node);
}
residentSize++;
}
/** Records a miss when the hot set is not full. */
private void onLirWarmupMiss(Node node) {
node.moveToTop(StackType.S);
node.status = Status.LIR;
sizeHot++;
}
/** Records a miss when the cold set is not full. */
private void onHirWarmupMiss(Node node) {
node.status = Status.HIR_RESIDENT;
node.moveToTop(StackType.Q);
}
/** Records a miss when the hot set is full. */
private void onFullMiss(Node node) {
// Upon accessing an HIR non-resident block X. This is a miss. We remove the HIR resident block
// at the bottom of stack Q (it then becomes a non-resident block) and evict it from the cache.
// Then, we load the requested block X into the freed buffer and place it at the top of stack
// S. There are two cases for the original location of block X: a) If X is in the stack S, we
// change its status to LIR and move the LIR block at the bottom of stack S to the top of
// stack Q with its status changed to HIR. A stack pruning is then conducted. This case is
// illustrated in the transition from state(a) to state(d) in Fig.2. b) If X is not in stack S,
// we leave its status unchanged and place it at the top of stack Q. This case is illustrated in
// the transition from state (a) to state (e) in Fig. 2.
node.status = Status.HIR_RESIDENT;
if (residentSize >= maximumSize) {
evict();
}
boolean isInStack = node.isInStack(StackType.S);
node.moveToTop(StackType.S);
if (isInStack) {
node.status = Status.LIR;
sizeHot++;
Node bottom = headS.prevS;
checkState(bottom.status == Status.LIR);
bottom.status = Status.HIR_RESIDENT;
bottom.removeFrom(StackType.S);
bottom.moveToTop(StackType.Q);
sizeHot--;
pruneStack();
} else {
node.moveToTop(StackType.Q);
}
}
private void pruneStack() {
// In the LIRS replacement, there is an operation called "stack pruning" on LIRS stack S, which
// removes the HIR blocks at the stack bottom until a LIR block sits there. This operation
// serves two purposes: 1) We ensure the block at the stack bottom 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 a chance to change their status from HIR to LIR since their
// recencies are larger than the new maximum recency of the LIR blocks.
for (;;) {
policyStats.recordOperation();
Node bottom = headS.prevS;
if ((bottom == headS) || (bottom.status == Status.LIR)) {
break;
} else if (bottom.status == Status.HIR_NON_RESIDENT) {
// the map only needs to hold non-resident entries that are on the stack
bottom.removeFrom(StackType.NR);
data.remove(bottom.key);
}
bottom.removeFrom(StackType.S);
}
// Bound the number of non-resident entries. While not described in the paper, the author's
// reference implementation provides a similar parameter to avoid uncontrolled growth.
Node node = headNR.prevNR;
while (sizeNR > maximumNonResidentSize) {
policyStats.recordOperation();
Node removed = node;
node = node.prevNR;
removed.removeFrom(StackType.NR);
removed.removeFrom(StackType.S);
data.remove(removed.key);
}
}
private void evict() {
// Once a free block is needed, the LIRS algorithm removes a resident HIR block from the bottom
// of stack Q for replacement. However, the replaced HIR block remains in stack S with its
// residence status changed to "non resident" if it is originally in the stack. We ensure the
// block in the bottom of the stack S is an LIR block by removing HIR blocks after it.
policyStats.recordEviction();
residentSize--;
Node bottom = headQ.prevQ;
bottom.removeFrom(StackType.Q);
bottom.status = Status.HIR_NON_RESIDENT;
if (bottom.isInStack(StackType.S)) {
bottom.moveToTop(StackType.NR);
} else {
// the map only needs to hold non-resident entries that are on the stack
data.remove(bottom.key);
}
pruneStack();
if (debug) {
evicted.add(bottom.key);
}
}
@Override
public PolicyStats stats() {
return policyStats;
}
@Override
public void finished() {
long resident = data.values().stream()
.filter(node -> node.status != Status.HIR_NON_RESIDENT)
.count();
checkState(resident == residentSize);
checkState(sizeHot <= maximumHotSize);
checkState(residentSize <= maximumSize);
checkState(sizeNR <= maximumNonResidentSize);
checkState(data.size() <= (maximumSize + maximumNonResidentSize));
checkState(sizeS == data.values().stream().filter(node -> node.isInS).count());
checkState(sizeQ == data.values().stream().filter(node -> node.isInQ).count());
if (debug) {
printLirs();
}
}
/** Prints out the internal state of the policy. */
private void printLirs() {
System.out.println("** LIRS stack TOP **");
for (Node n = headS.nextS; n != headS; n = n.nextS) {
checkState(n.isInS);
if (n.status == Status.HIR_NON_RESIDENT) {
System.out.println("<NR> " + n.key);
} else if (n.status == Status.HIR_RESIDENT) {
System.out.println("<RH> " + n.key);
} else {
System.out.println("<RL> " + n.key);
}
}
System.out.println("** LIRS stack BOTTOM **");
System.out.println("\n** LIRS queue END **");
for (Node n = headQ.nextQ; n != headQ; n = n.nextQ) {
checkState(n.isInQ);
System.out.println(n.key);
}
System.out.println("** LIRS queue front **");
System.out.println("\nLIRS EVICTED PAGE sequence:");
for (int i = 0; i < evicted.size(); i++) {
System.out.println("<" + i + "> " + evicted.get(i));
}
}
enum Status {
LIR,
HIR_RESIDENT,
HIR_NON_RESIDENT,
}
// S holds three types of blocks, LIR blocks, resident HIR blocks, non-resident HIR blocks
// Q holds all of the resident HIR blocks
// NR holds all of the non-resident HIR blocks
enum StackType {
// We store LIR blocks and HIR blocks with their recencies less than the maximum recency of the
// LIR blocks in a stack called LIRS stack S. S is similar to the LRU stack in operation but has
// a variable size.
S,
// To facilitate the search of the resident HIR blocks, we link all these blocks into a small
// stack, Q, with its size of Lhirs.
Q,
// Adaption to facilitate the search of the non-resident HIR blocks
NR,
}
// Each entry in the stack records the LIR/HIR status of a block and its residence status,
// indicating whether or not the block resides in the cache.
final class Node {
final long key;
Status status;
Node prevS;
Node nextS;
Node prevQ;
Node nextQ;
Node prevNR;
Node nextNR;
boolean isInS;
boolean isInQ;
boolean isInNR;
Node() {
key = Long.MIN_VALUE;
prevS = nextS = this;
prevQ = nextQ = this;
prevNR = nextNR = this;
}
Node(long key) {
this.key = key;
}
public boolean isInStack(StackType stackType) {
checkState(key != Long.MIN_VALUE);
if (stackType == StackType.S) {
return isInS;
} else if (stackType == StackType.Q) {
return isInQ;
} else if (stackType == StackType.NR) {
return isInNR;
} else {
throw new IllegalArgumentException();
}
}
public boolean isStackTop(StackType stackType) {
if (stackType == StackType.S) {
return (headS.nextS == this);
} else if (stackType == StackType.Q) {
return (headQ.nextQ == this);
} else if (stackType == StackType.NR) {
return (headNR.nextNR == this);
} else {
throw new IllegalArgumentException();
}
}
public void moveToTop(StackType stackType) {
if (isInStack(stackType)) {
removeFrom(stackType);
}
if (stackType == StackType.S) {
Node next = headS.nextS;
headS.nextS = this;
next.prevS = this;
this.nextS = next;
this.prevS = headS;
isInS = true;
sizeS++;
} else if (stackType == StackType.Q) {
Node next = headQ.nextQ;
headQ.nextQ = this;
next.prevQ = this;
this.nextQ = next;
this.prevQ = headQ;
isInQ = true;
sizeQ++;
} else if (stackType == StackType.NR) {
Node next = headNR.nextNR;
headNR.nextNR = this;
next.prevNR = this;
this.nextNR = next;
this.prevNR = headNR;
isInNR = true;
sizeNR++;
} else {
throw new IllegalArgumentException();
}
}
public void removeFrom(StackType stackType) {
checkState(isInStack(stackType));
if (stackType == StackType.S) {
prevS.nextS = nextS;
nextS.prevS = prevS;
prevS = nextS = null;
isInS = false;
sizeS--;
} else if (stackType == StackType.Q) {
prevQ.nextQ = nextQ;
nextQ.prevQ = prevQ;
prevQ = nextQ = null;
isInQ = false;
sizeQ--;
} else if (stackType == StackType.NR) {
prevNR.nextNR = nextNR;
nextNR.prevNR = prevNR;
prevNR = nextNR = null;
isInNR = false;
sizeNR--;
} else {
throw new IllegalArgumentException();
}
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("key", key)
.add("type", status)
.toString();
}
}
static final class LirsSettings extends BasicSettings {
public LirsSettings(Config config) {
super(config);
}
public double percentHot() {
return config().getDouble("lirs.percent-hot");
}
public double nonResidentMultiplier() {
return config().getDouble("lirs.non-resident-multiplier");
}
}
}