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