Skip to content

Commit f9af06a

Browse files
committed
Add incremental reset option of sketch to the simulator
TinyLFU resets is a periodic sweep of halving all counters in the sketch. An alternative is to reset incrementally, halving a table location every N additions and incrementing the cursor. This would remove a concern of an amortized O(1) cost being O(n) when on the period to a fully O(1) operation. This appears to work well for large caches with long traces. It has a negative impact on small caches with short traces. Earlier ad hoc experiments indicated this might be promising. Sadly that code was not kept and the current analysis doesn't match those observations. This requires more experimentation to see the feasibility of the approach.
1 parent a19dd73 commit f9af06a

File tree

14 files changed

+378
-70
lines changed

14 files changed

+378
-70
lines changed

gradle/dependencies.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ ext {
4747
jctools: '1.1',
4848
jimfs: '1.0',
4949
junit: '4.12',
50-
mockito: '2.0.32-beta',
51-
pax_exam: '4.7.0',
50+
mockito: '2.0.33-beta',
51+
pax_exam: '4.8.0',
5252
testng: '6.9.10',
5353
truth: '0.24',
5454
]

jcache/src/test/java/com/github/benmanes/caffeine/jcache/JCacheProfiler.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@
1717

1818
import static java.util.Objects.requireNonNull;
1919

20+
import java.util.Random;
2021
import java.util.concurrent.Executors;
2122
import java.util.concurrent.ForkJoinPool;
22-
import java.util.concurrent.ThreadLocalRandom;
2323
import java.util.concurrent.TimeUnit;
2424
import java.util.concurrent.atomic.LongAdder;
2525

@@ -45,9 +45,11 @@ public final class JCacheProfiler {
4545

4646
private final Cache<Integer, Boolean> cache;
4747
private final LongAdder count;
48+
private final Random random;
4849

4950
JCacheProfiler() {
50-
this.count = new LongAdder();
51+
random = new Random();
52+
count = new LongAdder();
5153
CachingProvider provider = Caching.getCachingProvider(PROVIDER_CLASS);
5254
CacheManager cacheManager = provider.getCacheManager(
5355
provider.getDefaultURI(), provider.getDefaultClassLoader());
@@ -59,7 +61,7 @@ public void start() {
5961
cache.put(i, Boolean.TRUE);
6062
}
6163
Runnable task = () -> {
62-
for (int i = ThreadLocalRandom.current().nextInt(); ; i++) {
64+
for (int i = random.nextInt(); ; i++) {
6365
Integer key = Math.abs(i % KEYS);
6466
if (READ) {
6567
requireNonNull(cache.get(key));

simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/BasicSettings.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,21 @@ public final class TinyLfuSettings {
115115
public String sketch() {
116116
return config().getString("tiny-lfu.sketch");
117117
}
118+
public CountMin4Settings countMin4() {
119+
return new CountMin4Settings();
120+
}
118121
public CountMin64Settings countMin64() {
119122
return new CountMin64Settings();
120123
}
121124

125+
public final class CountMin4Settings {
126+
public String reset() {
127+
return config().getString("tiny-lfu.count-min-4.reset");
128+
}
129+
public int increment() {
130+
return config().getInt("tiny-lfu.count-min-4.increment");
131+
}
132+
}
122133
public final class CountMin64Settings {
123134
public double eps() {
124135
return config().getDouble("tiny-lfu.count-min-64.eps");

simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/Simulator.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,12 @@ public enum Message { START, FINISH, ERROR }
6363
private final BasicSettings settings;
6464
private final Stopwatch stopwatch;
6565
private final Reporter report;
66-
private final Config config;
6766
private final Router router;
6867
private final int batchSize;
6968
private int remaining;
7069

7170
public Simulator() {
72-
config = getContext().system().settings().config().getConfig("caffeine.simulator");
71+
Config config = getContext().system().settings().config().getConfig("caffeine.simulator");
7372
settings = new BasicSettings(config);
7473

7574
List<Routee> routes = makeRoutes();

simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/admission/TinyLfu.java

Lines changed: 20 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,14 @@
1616
package com.github.benmanes.caffeine.cache.simulator.admission;
1717

1818
import com.clearspring.analytics.stream.frequency.CountMin64TinyLfu;
19-
import com.github.benmanes.caffeine.cache.CountMin4TinyLfu;
20-
import com.github.benmanes.caffeine.cache.RandomRemovalFrequencyTable;
21-
import com.github.benmanes.caffeine.cache.TinyCacheAdapter;
2219
import com.github.benmanes.caffeine.cache.simulator.BasicSettings;
20+
import com.github.benmanes.caffeine.cache.simulator.admission.countmin4.IncrementalResetCountMin4;
21+
import com.github.benmanes.caffeine.cache.simulator.admission.countmin4.PeriodicResetCountMin4;
22+
import com.github.benmanes.caffeine.cache.simulator.admission.perfect.PerfectFrequency;
23+
import com.github.benmanes.caffeine.cache.simulator.admission.table.RandomRemovalFrequencyTable;
24+
import com.github.benmanes.caffeine.cache.simulator.admission.tinycache.TinyCacheAdapter;
2325
import com.typesafe.config.Config;
2426

25-
import it.unimi.dsi.fastutil.longs.Long2IntMap;
26-
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
27-
2827
/**
2928
* Admits new entries based on the estimated frequency of its historic use.
3029
*
@@ -34,21 +33,29 @@ public final class TinyLfu implements Admittor {
3433
private final Frequency sketch;
3534

3635
public TinyLfu(Config config) {
36+
sketch = makeSketch(config);
37+
}
38+
39+
private Frequency makeSketch(Config config) {
3740
BasicSettings settings = new BasicSettings(config);
3841
String type = settings.tinyLfu().sketch();
3942
if (type.equalsIgnoreCase("count-min-4")) {
40-
sketch = new CountMin4TinyLfu(config);
43+
String reset = settings.tinyLfu().countMin4().reset();
44+
if (reset.equalsIgnoreCase("periodic")) {
45+
return new PeriodicResetCountMin4(config);
46+
} else if (reset.equalsIgnoreCase("incremental")) {
47+
return new IncrementalResetCountMin4(config);
48+
}
4149
} else if (type.equalsIgnoreCase("count-min-64")) {
42-
sketch = new CountMin64TinyLfu(config);
50+
return new CountMin64TinyLfu(config);
4351
} else if (type.equalsIgnoreCase("random-table")) {
44-
sketch = new RandomRemovalFrequencyTable(config);
52+
return new RandomRemovalFrequencyTable(config);
4553
} else if (type.equalsIgnoreCase("tiny-table")) {
46-
sketch = new TinyCacheAdapter(config);
54+
return new TinyCacheAdapter(config);
4755
} else if (type.equalsIgnoreCase("perfect-table")) {
48-
sketch = new PerfectTinyLfu(config);
49-
} else {
50-
throw new IllegalStateException("Unknown sketch type: " + type);
56+
return new PerfectFrequency(config);
5157
}
58+
throw new IllegalStateException("Unknown sketch type: " + type);
5259
}
5360

5461
@Override
@@ -62,38 +69,4 @@ public boolean admit(long candidateKey, long victimKey) {
6269
long victimFreq = sketch.frequency(victimKey);
6370
return candidateFreq > victimFreq;
6471
}
65-
66-
private static final class PerfectTinyLfu implements Frequency {
67-
private final Long2IntMap counts;
68-
private final int sampleSize;
69-
70-
private int size;
71-
72-
PerfectTinyLfu(Config config) {
73-
sampleSize = 10 * new BasicSettings(config).maximumSize();
74-
counts = new Long2IntOpenHashMap();
75-
}
76-
77-
@Override
78-
public int frequency(long e) {
79-
return counts.get(e);
80-
}
81-
82-
@Override
83-
public void increment(long e) {
84-
counts.put(e, counts.get(e) + 1);
85-
86-
size++;
87-
if (size == sampleSize) {
88-
reset();
89-
}
90-
}
91-
92-
private void reset() {
93-
for (Long2IntMap.Entry entry : counts.long2IntEntrySet()) {
94-
entry.setValue(entry.getValue() / 2);
95-
}
96-
size = (size / 2);
97-
}
98-
}
9972
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/*
2+
* Copyright 2015 Ben Manes. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.github.benmanes.caffeine.cache.simulator.admission.countmin4;
17+
18+
import static com.google.common.base.Preconditions.checkArgument;
19+
20+
import javax.annotation.Nonnegative;
21+
22+
import com.github.benmanes.caffeine.cache.simulator.BasicSettings;
23+
import com.github.benmanes.caffeine.cache.simulator.admission.Frequency;
24+
import com.typesafe.config.Config;
25+
26+
/**
27+
* A probabilistic multiset for estimating the popularity of an element within a time window. The
28+
* maximum frequency of an element is limited to 15 (4-bits) and extensions provide the aging
29+
* process.
30+
*
31+
* @author [email protected] (Ben Manes)
32+
*/
33+
abstract class CountMin4 implements Frequency {
34+
static final long[] SEED = new long[] { // A mixture of seeds from FNV-1a, CityHash, and Murmur3
35+
0xc3a5c85c97cb3127L, 0xb492b66fbe98f273L, 0x9ae16a3b2f90404fL, 0xcbf29ce484222325L};
36+
static final long RESET_MASK = 0x7777777777777777L;
37+
38+
final int randomSeed;
39+
40+
int tableMask;
41+
long[] table;
42+
43+
/**
44+
* Creates a frequency sketch that can accurately estimate the popularity of elements given
45+
* the maximum size of the cache.
46+
*/
47+
CountMin4(Config config) {
48+
BasicSettings settings = new BasicSettings(config);
49+
checkArgument(settings.randomSeed() != 0);
50+
randomSeed = settings.randomSeed();
51+
52+
ensureCapacity(settings.maximumSize());
53+
}
54+
55+
/**
56+
* Increases the capacity of this <tt>FrequencySketch</tt> instance, if necessary, to ensure that
57+
* it can accurately estimate the popularity of elements given the maximum size of the cache.
58+
*
59+
* @param maximumSize the maximum size of the cache
60+
*/
61+
public void ensureCapacity(@Nonnegative long maximumSize) {
62+
checkArgument(maximumSize >= 0);
63+
int maximum = (int) Math.min(maximumSize, Integer.MAX_VALUE >>> 1);
64+
if ((table != null) && (table.length >= maximum)) {
65+
return;
66+
}
67+
68+
table = new long[(maximum == 0) ? 1 : ceilingNextPowerOfTwo(maximum)];
69+
tableMask = Math.max(0, table.length - 1);
70+
}
71+
72+
/**
73+
* Returns the estimated number of occurrences of an element, up to the maximum (15).
74+
*
75+
* @param e the element to count occurrences of
76+
* @return the estimated number of occurrences of the element; possibly zero but never negative
77+
*/
78+
@Override
79+
@Nonnegative
80+
public int frequency(long e) {
81+
int hash = spread(Long.hashCode(e));
82+
int start = (hash & 3) << 2;
83+
int frequency = Integer.MAX_VALUE;
84+
for (int i = 0; i < 4; i++) {
85+
int index = indexOf(hash, i);
86+
int count = (int) ((table[index] >>> ((start + i) << 2)) & 0xfL);
87+
frequency = Math.min(frequency, count);
88+
}
89+
return frequency;
90+
}
91+
92+
/**
93+
* Increments the popularity of the element if it does not exceed the maximum (15). The popularity
94+
* of all elements will be periodically down sampled when the observed events exceeds a threshold.
95+
* This process provides a frequency aging to allow expired long term entries to fade away.
96+
*
97+
* @param e the element to add
98+
*/
99+
@Override
100+
public void increment(long e) {
101+
int hash = spread(Long.hashCode(e));
102+
int start = (hash & 3) << 2;
103+
104+
// Loop unrolling improves throughput by 5m ops/s
105+
int index0 = indexOf(hash, 0);
106+
int index1 = indexOf(hash, 1);
107+
int index2 = indexOf(hash, 2);
108+
int index3 = indexOf(hash, 3);
109+
110+
boolean added = incrementAt(index0, start);
111+
added |= incrementAt(index1, start + 1);
112+
added |= incrementAt(index2, start + 2);
113+
added |= incrementAt(index3, start + 3);
114+
115+
if (added) {
116+
tryReset();
117+
}
118+
}
119+
120+
/** Performs the aging process after an addition to allow old entries to fade away. */
121+
abstract void tryReset();
122+
123+
/**
124+
* Increments the specified counter by 1 if it is not already at the maximum value (15).
125+
*
126+
* @param i the table index (16 counters)
127+
* @param j the counter to increment
128+
* @return if incremented
129+
*/
130+
boolean incrementAt(int i, int j) {
131+
int offset = j << 2;
132+
long mask = (0xfL << offset);
133+
if ((table[i] & mask) != mask) {
134+
table[i] += (1L << offset);
135+
return true;
136+
}
137+
return false;
138+
}
139+
140+
/**
141+
* Returns the table index for the counter at the specified depth.
142+
*
143+
* @param item the element's hash
144+
* @param i the counter depth
145+
* @return the table index
146+
*/
147+
int indexOf(int item, int i) {
148+
long hash = SEED[i] * item;
149+
hash += hash >> 32;
150+
return ((int) hash) & tableMask;
151+
}
152+
153+
/**
154+
* Applies a supplemental hash function to a given hashCode, which defends against poor quality
155+
* hash functions.
156+
*/
157+
int spread(int x) {
158+
x = ((x >>> 16) ^ x) * 0x45d9f3b;
159+
x = ((x >>> 16) ^ x) * randomSeed;
160+
return (x >>> 16) ^ x;
161+
}
162+
163+
static int ceilingNextPowerOfTwo(int x) {
164+
// From Hacker's Delight, Chapter 3, Harry S. Warren Jr.
165+
return 1 << (Integer.SIZE - Integer.numberOfLeadingZeros(x - 1));
166+
}
167+
}

0 commit comments

Comments
 (0)