Skip to content

Commit

Permalink
Add incremental reset option of sketch to the simulator
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ben-manes committed Dec 19, 2015
1 parent a19dd73 commit f9af06a
Show file tree
Hide file tree
Showing 14 changed files with 378 additions and 70 deletions.
4 changes: 2 additions & 2 deletions gradle/dependencies.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ ext {
jctools: '1.1',
jimfs: '1.0',
junit: '4.12',
mockito: '2.0.32-beta',
pax_exam: '4.7.0',
mockito: '2.0.33-beta',
pax_exam: '4.8.0',
testng: '6.9.10',
truth: '0.24',
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@

import static java.util.Objects.requireNonNull;

import java.util.Random;
import java.util.concurrent.Executors;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.LongAdder;

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

private final Cache<Integer, Boolean> cache;
private final LongAdder count;
private final Random random;

JCacheProfiler() {
this.count = new LongAdder();
random = new Random();
count = new LongAdder();
CachingProvider provider = Caching.getCachingProvider(PROVIDER_CLASS);
CacheManager cacheManager = provider.getCacheManager(
provider.getDefaultURI(), provider.getDefaultClassLoader());
Expand All @@ -59,7 +61,7 @@ public void start() {
cache.put(i, Boolean.TRUE);
}
Runnable task = () -> {
for (int i = ThreadLocalRandom.current().nextInt(); ; i++) {
for (int i = random.nextInt(); ; i++) {
Integer key = Math.abs(i % KEYS);
if (READ) {
requireNonNull(cache.get(key));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,21 @@ public final class TinyLfuSettings {
public String sketch() {
return config().getString("tiny-lfu.sketch");
}
public CountMin4Settings countMin4() {
return new CountMin4Settings();
}
public CountMin64Settings countMin64() {
return new CountMin64Settings();
}

public final class CountMin4Settings {
public String reset() {
return config().getString("tiny-lfu.count-min-4.reset");
}
public int increment() {
return config().getInt("tiny-lfu.count-min-4.increment");
}
}
public final class CountMin64Settings {
public double eps() {
return config().getDouble("tiny-lfu.count-min-64.eps");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,12 @@ public enum Message { START, FINISH, ERROR }
private final BasicSettings settings;
private final Stopwatch stopwatch;
private final Reporter report;
private final Config config;
private final Router router;
private final int batchSize;
private int remaining;

public Simulator() {
config = getContext().system().settings().config().getConfig("caffeine.simulator");
Config config = getContext().system().settings().config().getConfig("caffeine.simulator");
settings = new BasicSettings(config);

List<Routee> routes = makeRoutes();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,14 @@
package com.github.benmanes.caffeine.cache.simulator.admission;

import com.clearspring.analytics.stream.frequency.CountMin64TinyLfu;
import com.github.benmanes.caffeine.cache.CountMin4TinyLfu;
import com.github.benmanes.caffeine.cache.RandomRemovalFrequencyTable;
import com.github.benmanes.caffeine.cache.TinyCacheAdapter;
import com.github.benmanes.caffeine.cache.simulator.BasicSettings;
import com.github.benmanes.caffeine.cache.simulator.admission.countmin4.IncrementalResetCountMin4;
import com.github.benmanes.caffeine.cache.simulator.admission.countmin4.PeriodicResetCountMin4;
import com.github.benmanes.caffeine.cache.simulator.admission.perfect.PerfectFrequency;
import com.github.benmanes.caffeine.cache.simulator.admission.table.RandomRemovalFrequencyTable;
import com.github.benmanes.caffeine.cache.simulator.admission.tinycache.TinyCacheAdapter;
import com.typesafe.config.Config;

import it.unimi.dsi.fastutil.longs.Long2IntMap;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;

/**
* Admits new entries based on the estimated frequency of its historic use.
*
Expand All @@ -34,21 +33,29 @@ public final class TinyLfu implements Admittor {
private final Frequency sketch;

public TinyLfu(Config config) {
sketch = makeSketch(config);
}

private Frequency makeSketch(Config config) {
BasicSettings settings = new BasicSettings(config);
String type = settings.tinyLfu().sketch();
if (type.equalsIgnoreCase("count-min-4")) {
sketch = new CountMin4TinyLfu(config);
String reset = settings.tinyLfu().countMin4().reset();
if (reset.equalsIgnoreCase("periodic")) {
return new PeriodicResetCountMin4(config);
} else if (reset.equalsIgnoreCase("incremental")) {
return new IncrementalResetCountMin4(config);
}
} else if (type.equalsIgnoreCase("count-min-64")) {
sketch = new CountMin64TinyLfu(config);
return new CountMin64TinyLfu(config);
} else if (type.equalsIgnoreCase("random-table")) {
sketch = new RandomRemovalFrequencyTable(config);
return new RandomRemovalFrequencyTable(config);
} else if (type.equalsIgnoreCase("tiny-table")) {
sketch = new TinyCacheAdapter(config);
return new TinyCacheAdapter(config);
} else if (type.equalsIgnoreCase("perfect-table")) {
sketch = new PerfectTinyLfu(config);
} else {
throw new IllegalStateException("Unknown sketch type: " + type);
return new PerfectFrequency(config);
}
throw new IllegalStateException("Unknown sketch type: " + type);
}

@Override
Expand All @@ -62,38 +69,4 @@ public boolean admit(long candidateKey, long victimKey) {
long victimFreq = sketch.frequency(victimKey);
return candidateFreq > victimFreq;
}

private static final class PerfectTinyLfu implements Frequency {
private final Long2IntMap counts;
private final int sampleSize;

private int size;

PerfectTinyLfu(Config config) {
sampleSize = 10 * new BasicSettings(config).maximumSize();
counts = new Long2IntOpenHashMap();
}

@Override
public int frequency(long e) {
return counts.get(e);
}

@Override
public void increment(long e) {
counts.put(e, counts.get(e) + 1);

size++;
if (size == sampleSize) {
reset();
}
}

private void reset() {
for (Long2IntMap.Entry entry : counts.long2IntEntrySet()) {
entry.setValue(entry.getValue() / 2);
}
size = (size / 2);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/*
* 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.admission.countmin4;

import static com.google.common.base.Preconditions.checkArgument;

import javax.annotation.Nonnegative;

import com.github.benmanes.caffeine.cache.simulator.BasicSettings;
import com.github.benmanes.caffeine.cache.simulator.admission.Frequency;
import com.typesafe.config.Config;

/**
* A probabilistic multiset for estimating the popularity of an element within a time window. The
* maximum frequency of an element is limited to 15 (4-bits) and extensions provide the aging
* process.
*
* @author [email protected] (Ben Manes)
*/
abstract class CountMin4 implements Frequency {
static final long[] SEED = new long[] { // A mixture of seeds from FNV-1a, CityHash, and Murmur3
0xc3a5c85c97cb3127L, 0xb492b66fbe98f273L, 0x9ae16a3b2f90404fL, 0xcbf29ce484222325L};
static final long RESET_MASK = 0x7777777777777777L;

final int randomSeed;

int tableMask;
long[] table;

/**
* Creates a frequency sketch that can accurately estimate the popularity of elements given
* the maximum size of the cache.
*/
CountMin4(Config config) {
BasicSettings settings = new BasicSettings(config);
checkArgument(settings.randomSeed() != 0);
randomSeed = settings.randomSeed();

ensureCapacity(settings.maximumSize());
}

/**
* Increases the capacity of this <tt>FrequencySketch</tt> instance, if necessary, to ensure that
* it can accurately estimate the popularity of elements given the maximum size of the cache.
*
* @param maximumSize the maximum size of the cache
*/
public void ensureCapacity(@Nonnegative long maximumSize) {
checkArgument(maximumSize >= 0);
int maximum = (int) Math.min(maximumSize, Integer.MAX_VALUE >>> 1);
if ((table != null) && (table.length >= maximum)) {
return;
}

table = new long[(maximum == 0) ? 1 : ceilingNextPowerOfTwo(maximum)];
tableMask = Math.max(0, table.length - 1);
}

/**
* Returns the estimated number of occurrences of an element, up to the maximum (15).
*
* @param e the element to count occurrences of
* @return the estimated number of occurrences of the element; possibly zero but never negative
*/
@Override
@Nonnegative
public int frequency(long e) {
int hash = spread(Long.hashCode(e));
int start = (hash & 3) << 2;
int frequency = Integer.MAX_VALUE;
for (int i = 0; i < 4; i++) {
int index = indexOf(hash, i);
int count = (int) ((table[index] >>> ((start + i) << 2)) & 0xfL);
frequency = Math.min(frequency, count);
}
return frequency;
}

/**
* Increments the popularity of the element if it does not exceed the maximum (15). The popularity
* of all elements will be periodically down sampled when the observed events exceeds a threshold.
* This process provides a frequency aging to allow expired long term entries to fade away.
*
* @param e the element to add
*/
@Override
public void increment(long e) {
int hash = spread(Long.hashCode(e));
int start = (hash & 3) << 2;

// Loop unrolling improves throughput by 5m ops/s
int index0 = indexOf(hash, 0);
int index1 = indexOf(hash, 1);
int index2 = indexOf(hash, 2);
int index3 = indexOf(hash, 3);

boolean added = incrementAt(index0, start);
added |= incrementAt(index1, start + 1);
added |= incrementAt(index2, start + 2);
added |= incrementAt(index3, start + 3);

if (added) {
tryReset();
}
}

/** Performs the aging process after an addition to allow old entries to fade away. */
abstract void tryReset();

/**
* Increments the specified counter by 1 if it is not already at the maximum value (15).
*
* @param i the table index (16 counters)
* @param j the counter to increment
* @return if incremented
*/
boolean incrementAt(int i, int j) {
int offset = j << 2;
long mask = (0xfL << offset);
if ((table[i] & mask) != mask) {
table[i] += (1L << offset);
return true;
}
return false;
}

/**
* Returns the table index for the counter at the specified depth.
*
* @param item the element's hash
* @param i the counter depth
* @return the table index
*/
int indexOf(int item, int i) {
long hash = SEED[i] * item;
hash += hash >> 32;
return ((int) hash) & tableMask;
}

/**
* Applies a supplemental hash function to a given hashCode, which defends against poor quality
* hash functions.
*/
int spread(int x) {
x = ((x >>> 16) ^ x) * 0x45d9f3b;
x = ((x >>> 16) ^ x) * randomSeed;
return (x >>> 16) ^ x;
}

static int ceilingNextPowerOfTwo(int x) {
// From Hacker's Delight, Chapter 3, Harry S. Warren Jr.
return 1 << (Integer.SIZE - Integer.numberOfLeadingZeros(x - 1));
}
}
Loading

0 comments on commit f9af06a

Please sign in to comment.