提交 dcc2163b authored 作者: Thomas Mueller's avatar Thomas Mueller

LIRS replacement algorithm

上级 a619d5c6
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
package org.h2.test.store; package org.h2.test.store;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map.Entry;
import java.util.Random; import java.util.Random;
import org.h2.dev.store.btree.CacheLirs; import org.h2.dev.store.btree.CacheLirs;
import org.h2.test.TestBase; import org.h2.test.TestBase;
...@@ -27,6 +28,7 @@ public class TestCache extends TestBase { ...@@ -27,6 +28,7 @@ public class TestCache extends TestBase {
public void test() throws Exception { public void test() throws Exception {
testEdgeCases(); testEdgeCases();
testClear();
testGetPutPeekRemove(); testGetPutPeekRemove();
testLimitHot(); testLimitHot();
testLimitNonResident(); testLimitNonResident();
...@@ -36,20 +38,44 @@ public class TestCache extends TestBase { ...@@ -36,20 +38,44 @@ public class TestCache extends TestBase {
} }
private void testEdgeCases() { private void testEdgeCases() {
CacheLirs<Integer, Integer> test = CacheLirs.newInstance(0); CacheLirs<Integer, Integer> test = CacheLirs.newInstance(1, 1);
test.put(1, 10); test.put(1, 10, 100);
assertEquals(10, test.get(1).intValue()); assertEquals(10, test.get(1).intValue());
try {
test.put(null, 10, 100);
fail();
} catch (NullPointerException e) {
// expected
}
try {
test.put(1, null, 100);
fail();
} catch (NullPointerException e) {
// expected
}
try {
test.setMaxMemory(0);
fail();
} catch (IllegalArgumentException e) {
// expected
}
try {
test.setAverageMemory(0);
fail();
} catch (IllegalArgumentException e) {
// expected
}
} }
private void testGetPutPeekRemove() { private void testGetPutPeekRemove() {
CacheLirs<Integer, Integer> test = CacheLirs.newInstance(4); CacheLirs<Integer, Integer> test = CacheLirs.newInstance(4, 1);
test.put(1, 10); test.put(1, 10);
test.put(2, 20); test.put(2, 20);
test.put(3, 30); test.put(3, 30);
assertNull(test.peek(4)); assertNull(test.peek(4));
assertNull(test.get(4)); assertNull(test.get(4));
test.put(4, 40); test.put(4, 40);
assertEquals("mem: 4 stack: 4 3 2 1 cold: non-resident:", toString(test)); verify(test, "mem: 4 stack: 4 3 2 1 cold: non-resident:");
// move middle to front // move middle to front
assertEquals(30, test.get(3).intValue()); assertEquals(30, test.get(3).intValue());
assertEquals(20, test.get(2).intValue()); assertEquals(20, test.get(2).intValue());
...@@ -58,77 +84,167 @@ public class TestCache extends TestBase { ...@@ -58,77 +84,167 @@ public class TestCache extends TestBase {
assertEquals(20, test.get(2).intValue()); assertEquals(20, test.get(2).intValue());
assertEquals(10, test.peek(1).intValue()); assertEquals(10, test.peek(1).intValue());
assertEquals(10, test.get(1).intValue()); assertEquals(10, test.get(1).intValue());
assertEquals("mem: 4 stack: 1 2 3 4 cold: non-resident:", toString(test)); verify(test, "mem: 4 stack: 1 2 3 4 cold: non-resident:");
test.put(3, 30); test.put(3, 30);
assertEquals("mem: 4 stack: 3 1 2 4 cold: non-resident:", toString(test)); verify(test, "mem: 4 stack: 3 1 2 4 cold: non-resident:");
// 5 is cold; will make 4 non-resident // 5 is cold; will make 4 non-resident
test.put(5, 50); test.put(5, 50);
assertEquals("mem: 4 stack: 5 3 1 2 cold: 5 non-resident: 4", toString(test)); verify(test, "mem: 4 stack: 5 3 1 2 cold: 5 non-resident: 4");
assertNull(test.peek(4)); assertNull(test.peek(4));
assertNull(test.get(4)); assertNull(test.get(4));
assertEquals(10, test.get(1).intValue()); assertEquals(10, test.get(1).intValue());
assertEquals(20, test.get(2).intValue()); assertEquals(20, test.get(2).intValue());
assertEquals(30, test.get(3).intValue()); assertEquals(30, test.get(3).intValue());
assertEquals("mem: 4 stack: 3 2 1 cold: 5 non-resident: 4", toString(test)); verify(test, "mem: 4 stack: 3 2 1 cold: 5 non-resident: 4");
assertEquals(50, test.get(5).intValue()); assertEquals(50, test.get(5).intValue());
assertEquals("mem: 4 stack: 5 3 2 1 cold: 5 non-resident: 4", toString(test)); verify(test, "mem: 4 stack: 5 3 2 1 cold: 5 non-resident: 4");
assertEquals(50, test.get(5).intValue()); assertEquals(50, test.get(5).intValue());
assertEquals("mem: 4 stack: 5 3 2 cold: 1 non-resident: 4", toString(test)); verify(test, "mem: 4 stack: 5 3 2 cold: 1 non-resident: 4");
// remove // remove
assertTrue(test.remove(5)); assertEquals(50, test.remove(5).intValue());
assertFalse(test.remove(5)); assertNull(test.remove(5));
assertEquals("mem: 3 stack: 3 2 1 cold: non-resident: 4", toString(test)); verify(test, "mem: 3 stack: 3 2 1 cold: non-resident: 4");
assertTrue(test.remove(4)); assertNull(test.remove(4));
assertFalse(test.remove(4)); verify(test, "mem: 3 stack: 3 2 1 cold: non-resident:");
assertEquals("mem: 3 stack: 3 2 1 cold: non-resident:", toString(test)); assertNull(test.remove(4));
verify(test, "mem: 3 stack: 3 2 1 cold: non-resident:");
test.put(4, 40); test.put(4, 40);
test.put(5, 50); test.put(5, 50);
assertEquals("mem: 4 stack: 5 4 3 2 cold: 5 non-resident: 1", toString(test)); verify(test, "mem: 4 stack: 5 4 3 2 cold: 5 non-resident: 1");
test.get(5); test.get(5);
test.get(2); test.get(2);
test.get(3); test.get(3);
test.get(4); test.get(4);
assertEquals("mem: 4 stack: 4 3 2 5 cold: 2 non-resident: 1", toString(test)); verify(test, "mem: 4 stack: 4 3 2 5 cold: 2 non-resident: 1");
assertTrue(test.remove(5)); assertEquals(50, test.remove(5).intValue());
assertEquals("mem: 3 stack: 4 3 2 cold: non-resident: 1", toString(test)); verify(test, "mem: 3 stack: 4 3 2 cold: non-resident: 1");
assertTrue(test.remove(2)); assertEquals(20, test.remove(2).intValue());
assertTrue(test.remove(1)); assertFalse(test.containsKey(1));
assertEquals("mem: 2 stack: 4 3 cold: non-resident:", toString(test)); assertNull(test.remove(1));
assertFalse(test.containsKey(1));
verify(test, "mem: 2 stack: 4 3 cold: non-resident:");
test.put(1, 10); test.put(1, 10);
test.put(2, 20); test.put(2, 20);
assertEquals("mem: 4 stack: 2 1 4 3 cold: non-resident:", toString(test)); verify(test, "mem: 4 stack: 2 1 4 3 cold: non-resident:");
test.get(1); test.get(1);
test.get(3); test.get(3);
test.get(4); test.get(4);
assertEquals("mem: 4 stack: 4 3 1 2 cold: non-resident:", toString(test)); verify(test, "mem: 4 stack: 4 3 1 2 cold: non-resident:");
assertTrue(test.remove(1)); assertEquals(10, test.remove(1).intValue());
assertEquals("mem: 3 stack: 4 3 2 cold: non-resident:", toString(test)); verify(test, "mem: 3 stack: 4 3 2 cold: non-resident:");
test.remove(2); test.remove(2);
test.remove(3); test.remove(3);
test.remove(4); test.remove(4);
// test clear // test clear
test.clear(); test.clear();
assertEquals("mem: 0 stack: cold: non-resident:", toString(test)); verify(test, "mem: 0 stack: cold: non-resident:");
// strange situation where there is only a non-resident entry
test.put(1, 10);
test.put(2, 20);
test.put(3, 30);
test.put(4, 40);
test.put(5, 50);
assertTrue(test.containsValue(50));
verify(test, "mem: 4 stack: 5 4 3 2 cold: 5 non-resident: 1");
test.put(1, 10);
verify(test, "mem: 4 stack: 1 5 4 3 2 cold: 1 non-resident: 5");
assertFalse(test.containsValue(50));
test.remove(2);
test.remove(3);
test.remove(4);
verify(test, "mem: 1 stack: 1 cold: non-resident: 5");
assertTrue(test.containsKey(1));
test.remove(1);
assertFalse(test.containsKey(1));
verify(test, "mem: 0 stack: cold: non-resident: 5");
assertFalse(test.containsKey(5));
assertTrue(test.isEmpty());
// verify that converting a hot to cold entry will prune the stack
test.clear();
test.put(1, 10);
test.put(2, 20);
test.put(3, 30);
test.put(4, 40);
test.put(5, 50);
test.get(4);
test.get(3);
verify(test, "mem: 4 stack: 3 4 5 2 cold: 5 non-resident: 1");
test.put(6, 60);
verify(test, "mem: 4 stack: 6 3 4 5 2 cold: 6 non-resident: 5 1");
// this will prune the stack (remove entry 5 as entry 2 becomes cold)
test.get(6);
verify(test, "mem: 4 stack: 6 3 4 cold: 2 non-resident: 5 1");
}
private void testClear() {
CacheLirs<Integer, Integer> test = CacheLirs.newInstance(40, 10);
for (int i = 0; i < 5; i++) {
test.put(i, 10 * i, 9);
}
verify(test, "mem: 36 stack: 4 3 2 1 cold: 4 non-resident: 0");
for (Entry<Integer, Integer> e : test.entrySet()) {
assertTrue(e.getKey() >= 1 && e.getKey() <= 4);
assertTrue(e.getValue() >= 10 && e.getValue() <= 40);
}
for (int x : test.values()) {
assertTrue(x >= 10 && x <= 40);
}
for (int x : test.keySet()) {
assertTrue(x >= 1 && x <= 4);
}
assertEquals(40, test.getMaxMemory());
assertEquals(10, test.getAverageMemory());
assertEquals(36, test.getUsedMemory());
assertEquals(4, test.size());
assertEquals(3, test.sizeHot());
assertEquals(1, test.sizeNonResident());
assertFalse(test.isEmpty());
// changing the limit is not supposed to modify the map
test.setMaxMemory(10);
assertEquals(10, test.getMaxMemory());
test.setMaxMemory(40);
test.setAverageMemory(1);
assertEquals(1, test.getAverageMemory());
test.setAverageMemory(10);
verify(test, "mem: 36 stack: 4 3 2 1 cold: 4 non-resident: 0");
// putAll uses the average memory
test.putAll(test);
verify(test, "mem: 40 stack: 4 3 2 1 cold: non-resident: 0");
test.clear();
verify(test, "mem: 0 stack: cold: non-resident:");
assertEquals(40, test.getMaxMemory());
assertEquals(10, test.getAverageMemory());
assertEquals(0, test.getUsedMemory());
assertEquals(0, test.size());
assertEquals(0, test.sizeHot());
assertEquals(0, test.sizeNonResident());
assertTrue(test.isEmpty());
} }
private void testLimitHot() { private void testLimitHot() {
CacheLirs<Integer, Integer> test = CacheLirs.newInstance(100); CacheLirs<Integer, Integer> test = CacheLirs.newInstance(100, 1);
for (int i = 0; i < 300; i++) { for (int i = 0; i < 300; i++) {
test.put(i, 10 * i); test.put(i, 10 * i);
} }
assertEquals(199, test.getSize()); assertEquals(100, test.size());
assertEquals(93, test.getHotSize()); assertEquals(99, test.sizeNonResident());
assertEquals(99, test.getNonResidentSize()); assertEquals(93, test.sizeHot());
} }
private void testLimitNonResident() { private void testLimitNonResident() {
CacheLirs<Integer, Integer> test = CacheLirs.newInstance(4); CacheLirs<Integer, Integer> test = CacheLirs.newInstance(4, 1);
for (int i = 0; i < 20; i++) { for (int i = 0; i < 20; i++) {
test.put(i, 10 * i); test.put(i, 10 * i);
} }
assertEquals("mem: 4 stack: 19 18 17 16 3 2 1 cold: 19 non-resident: 18 17 16", toString(test)); verify(test, "mem: 4 stack: 19 18 17 16 3 2 1 cold: 19 non-resident: 18 17 16");
} }
private void testBadHashMethod() { private void testBadHashMethod() {
...@@ -159,14 +275,14 @@ public class TestCache extends TestBase { ...@@ -159,14 +275,14 @@ public class TestCache extends TestBase {
} }
CacheLirs<BadHash, Integer> test = CacheLirs.newInstance(size * 2); CacheLirs<BadHash, Integer> test = CacheLirs.newInstance(size * 2, 1);
for (int i = 0; i < size; i++) { for (int i = 0; i < size; i++) {
test.put(new BadHash(i), i); test.put(new BadHash(i), i);
} }
for (int i = 0; i < size; i++) { for (int i = 0; i < size; i++) {
if (i % 3 == 0) { if (i % 3 == 0) {
assertTrue(test.remove(new BadHash(i))); assertEquals(i, test.remove(new BadHash(i)).intValue());
assertFalse(test.remove(new BadHash(i))); assertNull(test.remove(new BadHash(i)));
} }
} }
for (int i = 0; i < size; i++) { for (int i = 0; i < size; i++) {
...@@ -181,8 +297,8 @@ public class TestCache extends TestBase { ...@@ -181,8 +297,8 @@ public class TestCache extends TestBase {
} }
for (int i = 0; i < size; i++) { for (int i = 0; i < size; i++) {
if (i % 3 == 0) { if (i % 3 == 0) {
assertTrue(test.remove(new BadHash(i))); assertEquals(i, test.remove(new BadHash(i)).intValue());
assertFalse(test.remove(new BadHash(i))); assertNull(test.remove(new BadHash(i)));
} }
} }
for (int i = 0; i < size; i++) { for (int i = 0; i < size; i++) {
...@@ -198,7 +314,7 @@ public class TestCache extends TestBase { ...@@ -198,7 +314,7 @@ public class TestCache extends TestBase {
boolean log = false; boolean log = false;
int size = 20; int size = 20;
// cache size 11 (10 hot, 1 cold) // cache size 11 (10 hot, 1 cold)
CacheLirs<Integer, Integer> test = CacheLirs.newInstance(size / 2 + 1); CacheLirs<Integer, Integer> test = CacheLirs.newInstance(size / 2 + 1, 1);
// init the cache with some dummy entries // init the cache with some dummy entries
for (int i = 0; i < size; i++) { for (int i = 0; i < size; i++) {
test.put(-i, -i * 10); test.put(-i, -i * 10);
...@@ -248,7 +364,7 @@ public class TestCache extends TestBase { ...@@ -248,7 +364,7 @@ public class TestCache extends TestBase {
int size = 10; int size = 10;
Random r = new Random(1); Random r = new Random(1);
for (int j = 0; j < 100; j++) { for (int j = 0; j < 100; j++) {
CacheLirs<Integer, Integer> test = CacheLirs.newInstance(size / 2); CacheLirs<Integer, Integer> test = CacheLirs.newInstance(size / 2, 1);
HashMap<Integer, Integer> good = New.hashMap(); HashMap<Integer, Integer> good = New.hashMap();
for (int i = 0; i < 10000; i++) { for (int i = 0; i < 10000; i++) {
int key = r.nextInt(size); int key = r.nextInt(size);
...@@ -306,4 +422,14 @@ public class TestCache extends TestBase { ...@@ -306,4 +422,14 @@ public class TestCache extends TestBase {
return buff.toString(); return buff.toString();
} }
private <K, V> void verify(CacheLirs<K, V> cache, String expected) {
String got = toString(cache);
assertEquals(expected, got);
int mem = 0;
for (K k : cache.keySet()) {
mem += cache.getMemory(k);
}
assertEquals(mem, cache.getUsedMemory());
}
} }
...@@ -7,33 +7,82 @@ ...@@ -7,33 +7,82 @@
package org.h2.dev.store.btree; package org.h2.dev.store.btree;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set;
/** /**
* A cache. * A LIRS cache.
* <p> * <p>
* This implementation is not multi-threading save. * This implementation is not multi-threading save. Null keys or null values are
* * not allowed. There is no guard against bad hash functions, so it is important
* It is important to use a good hash function for the key (there is no guard against bad hash functions). * to the hash function of the key is good.
* <p>
* Each each entry is assigned a distinct memory size, and the cache will try to
* use at most the specified amount of memory. The memory unit is not relevant,
* however it is suggested to use bytes as the unit.
* <p> * <p>
* An implementation of the LIRS replacement algorithm from Xiaodong Zhang and * An implementation of the LIRS replacement algorithm from Xiaodong Zhang and
* Song Jiang as described in * Song Jiang as described in
* http://www.cse.ohio-state.edu/~zhang/lirs-sigmetrics-02.html with a few * http://www.cse.ohio-state.edu/~zhang/lirs-sigmetrics-02.html with a few
* smaller changes: An additional queue for non-resident entries is used, to * smaller changes: An additional queue for non-resident entries is used, to
* prevent unbound memory usage. The maximum size of this queue is at most the * prevent unbound memory usage. The maximum size of this queue is at most the
* size of the rest of the stack. This implementation allows each entry to have * size of the rest of the stack. About 5% of the mapped entries are cold.
* a distinct memory size. At most 6.25% of the mapped entries are cold. *
* @author Thomas Mueller
* *
* @param <K> the key type * @param <K> the key type
* @param <V> the value type * @param <V> the value type
*/ */
public class CacheLirs<K, V> { public class CacheLirs<K, V> implements Map<K, V> {
/**
* The maximum memory this cache should use.
*/
private long maxMemory; private long maxMemory;
private long currentMemory;
/**
* The average memory used by one entry.
*/
private int averageMemory; private int averageMemory;
private int mapSize, stackSize, queueSize, queue2Size;
/**
* The currently used memory.
*/
private long usedMemory;
/**
* The number of entries in the map. This includes all hot and cold entries.
*/
private int mapSize;
/**
* The LIRS stack size. This includes all hot and some of the cold entries.
*/
private int stackSize;
/**
* The size of the LIRS queue for resident cold entries.
*/
private int queueSize;
/**
* The size of the LIRS queue for non-resident cold entries.
*/
private int queue2Size;
/**
* The map entries. The size is always a power of 2.
*/
private Entry<K, V>[] entries; private Entry<K, V>[] entries;
/**
* The bit mask that is applied to the key hash code to get the map index.
* The value is the size of the entries array minus one.
*/
private int mask; private int mask;
/** /**
...@@ -59,43 +108,56 @@ public class CacheLirs<K, V> { ...@@ -59,43 +108,56 @@ public class CacheLirs<K, V> {
* @param averageMemory the average memory usage of an object * @param averageMemory the average memory usage of an object
*/ */
private CacheLirs(long maxMemory, int averageMemory) { private CacheLirs(long maxMemory, int averageMemory) {
this.maxMemory = maxMemory; setMaxMemory(maxMemory);
this.averageMemory = averageMemory; setAverageMemory(averageMemory);
clear(); clear();
} }
/** /**
* Create a new cache. * Create a new cache with the given size in number of entries.
* *
* @param size the maximum number of elements * @param maxMemory the maximum memory to use (1 or larger)
* @param averageMemory the average memory (1 or larger)
* @return the cache
*/ */
public static <K, V> CacheLirs<K, V> newInstance(int size) { public static <K, V> CacheLirs<K, V> newInstance(int maxMemory, int averageMemory) {
return new CacheLirs<K, V>(size, 1); return new CacheLirs<K, V>(maxMemory, averageMemory);
} }
/** /**
* Clear the cache. * Clear the cache.
*/ */
public void clear() { public void clear() {
// calculate the size of the map array
// assume a fill factor of at most 75%
long maxLen = (long) (maxMemory / averageMemory / 0.75); long maxLen = (long) (maxMemory / averageMemory / 0.75);
// the size needs to be a power of 2
long l = 8; long l = 8;
while (l < maxLen) { while (l < maxLen) {
l += l; l += l;
} }
// the array size is at most 2^31 elements
int len = (int) Math.min(1L << 31, l); int len = (int) Math.min(1L << 31, l);
// the bit mask has all bits set
mask = len - 1; mask = len - 1;
// initialize the stack and queue heads
stack = new Entry<K, V>(); stack = new Entry<K, V>();
stack.stackPrev = stack.stackNext = stack; stack.stackPrev = stack.stackNext = stack;
queue = new Entry<K, V>(); queue = new Entry<K, V>();
queue.queuePrev = queue.queueNext = queue; queue.queuePrev = queue.queueNext = queue;
queue2 = new Entry<K, V>(); queue2 = new Entry<K, V>();
queue2.queuePrev = queue2.queueNext = queue2; queue2.queuePrev = queue2.queueNext = queue2;
// first set to null - avoiding out of memory // first set to null - avoiding out of memory
entries = null; entries = null;
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
Entry<K, V>[] e = new Entry[len]; Entry<K, V>[] e = new Entry[len];
entries = e; entries = e;
currentMemory = 0;
mapSize = 0;
usedMemory = 0;
stackSize = queueSize = queue2Size = 0; stackSize = queueSize = queue2Size = 0;
} }
...@@ -111,22 +173,38 @@ public class CacheLirs<K, V> { ...@@ -111,22 +173,38 @@ public class CacheLirs<K, V> {
return e == null ? null : e.value; return e == null ? null : e.value;
} }
/**
* Get the memory used for the given key.
*
* @param key the key
* @return the memory, or 0 if there is no resident entry
*/
public int getMemory(K key) {
Entry<K, V> e = find(key);
return e == null ? null : e.memory;
}
/** /**
* Get an entry if the entry is cached. This method adjusts the internal * Get an entry if the entry is cached. This method adjusts the internal
* state of the cache, to ensure commonly used entries stay in the cache. * state of the cache, to ensure commonly used entries stay in the cache.
* *
* @param key the key * @param key the key (may not be null)
* @return the value, or null if not found * @return the value, or null if not found
*/ */
public V get(K key) { public V get(Object key) {
Entry<K, V> e = find(key); Entry<K, V> e = find(key);
if (e == null || e.value == null) { if (e == null || e.value == null) {
// either the entry was not found, or it was a non-resident entry
return null; return null;
} else if (e.isHot()) { } else if (e.isHot()) {
if (e != stack.stackNext) { if (e != stack.stackNext) {
// move a hot entries to the top of the stack
// unless it is already there
boolean wasEnd = e == stack.stackPrev; boolean wasEnd = e == stack.stackPrev;
removeFromStack(e); removeFromStack(e);
if (wasEnd) { if (wasEnd) {
// if moving the last entry, the last entry
// could not be cold, which is not allowed
pruneStack(); pruneStack();
} }
addToStack(e); addToStack(e);
...@@ -134,11 +212,17 @@ public class CacheLirs<K, V> { ...@@ -134,11 +212,17 @@ public class CacheLirs<K, V> {
} else { } else {
removeFromQueue(e); removeFromQueue(e);
if (e.stackNext != null) { if (e.stackNext != null) {
// resident cold entries become hot
// if they are on the stack
removeFromStack(e); removeFromStack(e);
// which means a hot entry needs to become cold
convertOldestHotToCold(); convertOldestHotToCold();
} else { } else {
// cold entries that are not on the stack
// move to the front of the queue
addToQueue(queue, e); addToQueue(queue, e);
} }
// in any case, the cold entry is moved to the top of the stack
addToStack(e); addToStack(e);
} }
return e.value; return e.value;
...@@ -148,11 +232,11 @@ public class CacheLirs<K, V> { ...@@ -148,11 +232,11 @@ public class CacheLirs<K, V> {
* Add an entry to the cache. This method is the same as adding an entry * Add an entry to the cache. This method is the same as adding an entry
* with the average memory size. * with the average memory size.
* *
* @param key the key * @param key the key (may not be null)
* @param value the value * @param value the value (may not be null)
*/ */
public void put(K key, V value) { public V put(K key, V value) {
put(key, value, averageMemory); return put(key, value, averageMemory);
} }
/** /**
...@@ -160,63 +244,77 @@ public class CacheLirs<K, V> { ...@@ -160,63 +244,77 @@ public class CacheLirs<K, V> {
* yet. This method will usually mark unknown entries as cold and known * yet. This method will usually mark unknown entries as cold and known
* entries as hot. * entries as hot.
* *
* @param key the key * @param key the key (may not be null)
* @param value the value * @param value the value (may not be null)
* @param memory the memory used for the given entry * @param memory the memory used for the given entry
*/ */
public void put(K key, V value, int memory) { public V put(K key, V value, int memory) {
if (find(key) != null) { if (value == null) {
throw new NullPointerException();
}
V old;
Entry<K, V> e = find(key);
if (e == null) {
old = null;
} else {
old = e.value;
remove(key); remove(key);
} }
Entry<K, V> e = new Entry<K, V>(); e = new Entry<K, V>();
e.key = key; e.key = key;
e.value = value; e.value = value;
e.memory = memory; e.memory = memory;
int index = key.hashCode() & mask; int index = key.hashCode() & mask;
e.chained = entries[index]; e.mapNext = entries[index];
entries[index] = e; entries[index] = e;
currentMemory += memory; usedMemory += memory;
if (currentMemory > maxMemory && mapSize > 0) { if (usedMemory > maxMemory && mapSize > 0) {
// an old entry needs to be removed
evict(e); evict(e);
} }
mapSize++; mapSize++;
// added entries are always added to the stack
addToStack(e); addToStack(e);
return old;
} }
/** /**
* Remove an entry. * Remove an entry.
* *
* @param key the key * @param key the key (may not be null)
* @return true if the entry was found (resident or non-resident) * @return true if the entry was found (resident or non-resident)
*/ */
public boolean remove(K key) { public V remove(Object key) {
int hash = key.hashCode(); int hash = key.hashCode();
int index = hash & mask; int index = hash & mask;
Entry<K, V> e = entries[index]; Entry<K, V> e = entries[index];
if (e == null) { if (e == null) {
return false; return null;
} }
V old;
if (e.key.equals(key)) { if (e.key.equals(key)) {
entries[index] = e.chained; old = e.value;
entries[index] = e.mapNext;
} else { } else {
Entry<K, V> last; Entry<K, V> last;
do { do {
last = e; last = e;
e = e.chained; e = e.mapNext;
if (e == null) { if (e == null) {
return false; return null;
} }
} while (!e.key.equals(key)); } while (!e.key.equals(key));
last.chained = e.chained; old = e.value;
last.mapNext = e.mapNext;
} }
mapSize--; mapSize--;
currentMemory -= e.memory; usedMemory -= e.memory;
if (e.stackNext != null) { if (e.stackNext != null) {
removeFromStack(e); removeFromStack(e);
} }
if (e.isHot()) { if (e.isHot()) {
// when removing a hot entry, convert the newest cold entry to hot, // when removing a hot entry, the newest cold entry gets hot,
// so that we keep the number of hot entries // so the number of hot entries does not change
e = queue.queueNext; e = queue.queueNext;
if (e != queue) { if (e != queue) {
removeFromQueue(e); removeFromQueue(e);
...@@ -228,21 +326,33 @@ public class CacheLirs<K, V> { ...@@ -228,21 +326,33 @@ public class CacheLirs<K, V> {
removeFromQueue(e); removeFromQueue(e);
} }
pruneStack(); pruneStack();
return true; return old;
} }
/**
* Evict cold entries (resident and non-resident) until the memory limit is
* reached.
*
* @param newCold a new cold entry
*/
private void evict(Entry<K, V> newCold) { private void evict(Entry<K, V> newCold) {
// ensure there are not too many hot entries:
// left shift of 5 is multiplication by 32, that means if there are less
// than 1/32 (3.125%) cold entries, a new hot entry needs to become cold
while ((queueSize << 5) < mapSize) { while ((queueSize << 5) < mapSize) {
convertOldestHotToCold(); convertOldestHotToCold();
} }
// the new cold entry is at the top of the queue
addToQueue(queue, newCold); addToQueue(queue, newCold);
while (currentMemory > maxMemory) { // the oldest resident cold entries become non-resident
while (usedMemory > maxMemory) {
Entry<K, V> e = queue.queuePrev; Entry<K, V> e = queue.queuePrev;
currentMemory -= e.memory; usedMemory -= e.memory;
removeFromQueue(e); removeFromQueue(e);
e.value = null; e.value = null;
e.memory = 0; e.memory = 0;
addToQueue(queue2, e); addToQueue(queue2, e);
// the size of the non-resident-cold entries needs to be limited
while (queue2Size + queue2Size > stackSize) { while (queue2Size + queue2Size > stackSize) {
e = queue2.queuePrev; e = queue2.queuePrev;
remove(e.key); remove(e.key);
...@@ -251,27 +361,41 @@ public class CacheLirs<K, V> { ...@@ -251,27 +361,41 @@ public class CacheLirs<K, V> {
} }
private void convertOldestHotToCold() { private void convertOldestHotToCold() {
// the last entry of the stack is known to be hot
Entry<K, V> last = stack.stackPrev; Entry<K, V> last = stack.stackPrev;
// remove from stack - which is done anyway in the stack pruning, but we
// can do it here as well
removeFromStack(last); removeFromStack(last);
// adding an entry to the queue will make it cold
addToQueue(queue, last); addToQueue(queue, last);
pruneStack(); pruneStack();
} }
/**
* Ensure the last entry of the stack is cold.
*/
private void pruneStack() { private void pruneStack() {
while (true) { while (true) {
Entry<K, V> last = stack.stackPrev; Entry<K, V> last = stack.stackPrev;
if (last == stack || last.isHot()) { if (last == stack || last.isHot()) {
break; break;
} }
// the cold entry is still in the queue
removeFromStack(last); removeFromStack(last);
} }
} }
private Entry<K, V> find(K key) { /**
* Try to find an entry in the map.
*
* @param key the key
* @return the entry (might be a non-resident)
*/
private Entry<K, V> find(Object key) {
int hash = key.hashCode(); int hash = key.hashCode();
Entry<K, V> e = entries[hash & mask]; Entry<K, V> e = entries[hash & mask];
while (e != null && !e.key.equals(key)) { while (e != null && !e.key.equals(key)) {
e = e.chained; e = e.mapNext;
} }
return e; return e;
} }
...@@ -323,53 +447,114 @@ public class CacheLirs<K, V> { ...@@ -323,53 +447,114 @@ public class CacheLirs<K, V> {
} }
/** /**
* Get the number of mapped entries (resident and non-resident). * Get the list of keys. This method allows to view the internal state of
* the cache.
* *
* @return the number of entries * @param cold if true, the keys for the cold entries are returned
* @param nonResident true for non-resident entries
* @return the key list
*/ */
public int getSize() { public List<K> keys(boolean cold, boolean nonResident) {
return mapSize; ArrayList<K> s = new ArrayList<K>();
if (cold) {
Entry<K, V> start = nonResident ? queue2 : queue;
for (Entry<K, V> e = start.queueNext; e != start; e = e.queueNext) {
s.add(e.key);
}
} else {
for (Entry<K, V> e = stack.stackNext; e != stack; e = e.stackNext) {
s.add(e.key);
}
}
return s;
} }
/** /**
* Get the number of hot entries. * Get the number of resident entries.
* *
* @return the number of entries * @return the number of entries
*/ */
public int getHotSize() { public int size() {
return mapSize - queueSize - queue2Size; return mapSize - queue2Size;
} }
/** /**
* Get the number of non-resident entries. * Check whether there are any resident entries in the map.
* *
* @return the number of entries * @return true if there are no keys
*/ */
public int getNonResidentSize() { public boolean isEmpty() {
return queue2Size; return size() == 0;
} }
/** /**
* Get the list of keys for this map. This method allows to view the internal * Check whether there is a resident entry for the given key.
* state of the cache.
* *
* @param cold if true only the keys for the cold entries are returned * @return true if the key is in the map
* @param nonResident true for non-resident entries
* @return the key set
*/ */
public List<K> keys(boolean cold, boolean nonResident) { public boolean containsKey(Object key) {
ArrayList<K> s = new ArrayList<K>(); Entry<K, V> e = find(key);
if (cold) { return e != null && e.value != null;
Entry<K, V> start = nonResident ? queue2 : queue;
for (Entry<K, V> e = start.queueNext; e != start; e = e.queueNext) {
s.add(e.key);
} }
} else {
/**
* Check whether there are any keys for the given value.
*
* @return true if there is a key for this value
*/
public boolean containsValue(Object value) {
return values().contains(value);
}
public void putAll(Map<? extends K, ? extends V> m) {
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
put(e.getKey(), e.getValue());
}
}
public Set<K> keySet() {
HashSet<K> set = new HashSet<K>();
for (Entry<K, V> e = stack.stackNext; e != stack; e = e.stackNext) { for (Entry<K, V> e = stack.stackNext; e != stack; e = e.stackNext) {
s.add(e.key); set.add(e.key);
} }
for (Entry<K, V> e = queue.queueNext; e != queue; e = e.queueNext) {
set.add(e.key);
} }
return s; return set;
}
public Collection<V> values() {
ArrayList<V> list = new ArrayList<V>();
for (K k : keySet()) {
list.add(get(k));
}
return list;
}
public Set<Map.Entry<K, V>> entrySet() {
HashMap<K, V> map = new HashMap<K, V>();
for (K k : keySet()) {
map.put(k, find(k).value);
}
return map.entrySet();
}
/**
* Get the number of hot entries in the cache.
*
* @return the number of entries
*/
public int sizeHot() {
return mapSize - queueSize - queue2Size;
}
/**
* Get the number of non-resident entries in the cache.
*
* @return the number of entries
*/
public int sizeNonResident() {
return queue2Size;
} }
/** /**
...@@ -378,12 +563,56 @@ public class CacheLirs<K, V> { ...@@ -378,12 +563,56 @@ public class CacheLirs<K, V> {
* @return the used memory * @return the used memory
*/ */
public long getUsedMemory() { public long getUsedMemory() {
return currentMemory; return usedMemory;
}
/**
* Set the maximum memory this cache should use. This will not immediately
* cause entries to get removed however; it will only change the limit.
*
* @param maxMemory the maximum size (1 or larger)
*/
public void setMaxMemory(long maxMemory) {
if (maxMemory <= 0) {
throw new IllegalArgumentException("Max memory must be larger than 0");
}
this.maxMemory = maxMemory;
}
/**
* Get the maximum memory to use.
*
* @return the maximum memory
*/
public long getMaxMemory() {
return maxMemory;
}
/**
* Set the average memory used per entry. It is used to calculate the size
* of the map.
*
* @param averageMemory the average memory used (1 or larger)
*/
public void setAverageMemory(int averageMemory) {
if (averageMemory <= 0) {
throw new IllegalArgumentException("Average memory must be larger than 0");
}
this.averageMemory = averageMemory;
}
/**
* Get the average memory used per entry.
*
* @return the average memory
*/
public int getAverageMemory() {
return averageMemory;
} }
/** /**
* A cache entry. Each entry is either hot (low inter-reference recency; * A cache entry. Each entry is either hot (low inter-reference recency;
* lir), cold (high inter-reference recency; hir), or non-resident-cold. Hot * LIR), cold (high inter-reference recency; HIR), or non-resident-cold. Hot
* entries are in the stack only. Cold entries are in the queue, and may be * entries are in the stack only. Cold entries are in the queue, and may be
* in the stack. Non-resident-cold entries have their value set to null and * in the stack. Non-resident-cold entries have their value set to null and
* are in the stack and in the non-resident queue. * are in the stack and in the non-resident queue.
...@@ -392,13 +621,53 @@ public class CacheLirs<K, V> { ...@@ -392,13 +621,53 @@ public class CacheLirs<K, V> {
* @param <V> the value type * @param <V> the value type
*/ */
static class Entry<K, V> { static class Entry<K, V> {
/**
* The key.
*/
K key; K key;
/**
* The value. Set to null for non-resident-cold entries.
*/
V value; V value;
/**
* The estimated memory used.
*/
int memory; int memory;
Entry<K, V> stackPrev, stackNext;
Entry<K, V> queuePrev, queueNext;
Entry<K, V> chained;
/**
* The next entry in the stack.
*/
Entry<K, V> stackNext;
/**
* The previous entry in the stack.
*/
Entry<K, V> stackPrev;
/**
* The next entry in the queue (either the resident queue or the
* non-resident queue).
*/
Entry<K, V> queueNext;
/**
* The previous entry in the queue.
*/
Entry<K, V> queuePrev;
/**
* The next entry in the map
*/
Entry<K, V> mapNext;
/**
* Whether this entry is hot. Cold entries are in one of the two queues.
*
* @return whether the entry is hot
*/
boolean isHot() { boolean isHot() {
return queueNext == null; return queueNext == null;
} }
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论