Skip to content

Commit

Permalink
add a clairvoyant admission policy to the simulator
Browse files Browse the repository at this point in the history
Like Bélády's optimal policy for eviction, this estimates the upper
bound for an admission policy by using future knowledge to make the
best decision. The candidate entry is only admitted if its next access
time is less than the victim's.

While the optimal replacement policy is able to dictate the working
set, the optimal admission policy is only able to advise whether to
keep the given candidate or victim entry. Thus, the eviction policy's
choices may result in a sequence where a more naive admission policy
outperforms the clairvoyant one. The intent is to show the upper
bounds of an adaptive admission policy based on its best guess rather
than the upper bound of the overall cache hit rate.
  • Loading branch information
ben-manes committed Jul 25, 2024
1 parent 56a7055 commit 98895ff
Show file tree
Hide file tree
Showing 18 changed files with 199 additions and 80 deletions.
18 changes: 9 additions & 9 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
asm = "9.7"
auto-value = "1.11.0"
awaitility = "4.2.1"
bcel = "6.9.0"
bcel = "6.10.0"
bnd = "7.0.0"
bouncycastle-jdk18on = "1.78.1"
cache2k = "2.6.1.Final"
Expand All @@ -13,18 +13,18 @@ coherence = "22.06.2"
commons-collections4 = "4.4"
commons-compress = "1.26.2"
commons-io = "2.16.1"
commons-lang3 = "3.14.0"
commons-lang3 = "3.15.0"
commons-math3 = "3.6.1"
commons-text = "1.12.0"
concurrentlinkedhashmap = "1.4.2"
config = "1.4.3"
coveralls = "2.12.2"
dependency-check = "10.0.2"
dependency-check = "10.0.3"
eclipse-collections = "12.0.0.M3"
ehcache3 = "3.10.8"
errorprone = "2.28.0"
errorprone = "2.29.2"
errorprone-plugin = "4.0.1"
errorprone-support = "0.16.1"
errorprone-support = "0.17.0"
expiring-map = "0.5.11"
fast-filter = "1.0.2"
fastutil = "8.5.13"
Expand All @@ -36,8 +36,8 @@ forbidden-apis = "3.7"
google-java-format = "1.22.0"
guava = "33.2.1-jre"
guice = "6.0.0"
h2 = "2.2.224"
hamcrest = "2.2"
h2 = "2.3.230"
hamcrest = "3.0-rc1"
hazelcast = "5.3.7"
httpclient = "4.5.14"
idea = "1.1.8"
Expand All @@ -63,7 +63,7 @@ jsoup = "1.18.1"
junit-testng = "1.0.5"
junit4 = "4.13.2"
junit5 = "5.11.0-M2"
jvm-dependency-conflict-resolution = "2.1.1"
jvm-dependency-conflict-resolution = "2.1.2"
kotlin = "2.0.0"
lincheck = "2.32"
mockito = "5.12.0"
Expand Down Expand Up @@ -96,7 +96,7 @@ versions = "0.51.0"
xz = "1.9"
ycsb = "0.17.0"
zero-allocation-hashing = "0.26ea0"
zstd = "1.5.6-3"
zstd = "1.5.6-4"

[libraries]
asm-bom = { module = "org.ow2.asm:asm-bom", version.ref = "asm" }
Expand Down
15 changes: 10 additions & 5 deletions gradle/plugins/src/main/kotlin/ProjectExtensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,16 @@ fun Project.version(major: Int, minor: Int, patch: Int, releaseBuild: Boolean) {
version = "$major.$minor.$patch" + if (releaseBuild) "" else "-SNAPSHOT"
}

fun Project.javaExecJvmArgs(): List<String> {
val arguments = findProperty("jvmArgs") as String?
fun Project.defaultJvmArgs(): List<String> {
val jvmArgs = mutableListOf("-Xmx4g")
if (System.getenv("GRAALVM") == "true") {
jvmArgs += listOf(
"-XX:+UnlockExperimentalVMOptions", "-Dgraal.ShowConfiguration=info",
"-XX:+EnableJVMCI", "-XX:+UseJVMCICompiler", "-XX:+EagerJVMCI")
}
val arguments = findProperty("jvmArgs") as String?
if (arguments != null) {
jvmArgs.addAll(arguments.split(","))
jvmArgs += arguments.split(",")
}
return jvmArgs
}
Expand All @@ -30,13 +35,13 @@ fun isCI(): Boolean {

val DisableStrongEncapsulationJvmArgs = listOf(
"--add-exports", "jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED",
"--add-exports", "jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED",
"--add-exports", "jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED",
"--add-exports", "jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED",
"--add-exports", "jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
"--add-exports", "jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED",
"--add-exports", "jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED",
"--add-exports", "jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED",
"--add-exports", "jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED",
"--add-exports", "jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED",
"--add-opens", "jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED",
"--add-opens", "jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED",
"--add-opens", "jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED")
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,14 @@ jmh {
iterations = 3
timeUnit = "s"

jvmArgs = defaultJvmArgs()
failOnError = true
forceGC = true
fork = 1

resultsFile = layout.buildDirectory.file("reports/jmh/results.json")
resultFormat = "json"

val jvmArguments = mutableListOf("-Xmx2G")
if (System.getenv("GRAALVM") == "true") {
jvmArguments += listOf(
"-XX:+UnlockExperimentalVMOptions", "-Dgraal.ShowConfiguration=info",
"-XX:+EnableJVMCI", "-XX:+UseJVMCICompiler", "-XX:+EagerJVMCI")
}
jvmArgs = jvmArguments

val includePattern: String? by project
if (includePattern != null) {
includes = listOf(includePattern)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,7 @@ tasks.withType<Test>().configureEach {
if ("debug" in systemProperties) {
jvmArgs("-Xdebug", "-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005")
}
if (environment["GRAALVM"] == "true") {
jvmArgs("-XX:+UnlockExperimentalVMOptions", "-Dgraal.ShowConfiguration=info",
"-XX:+EnableJVMCI", "-XX:+UseJVMCICompiler", "-XX:+EagerJVMCI")
}
jvmArgs(defaultJvmArgs())
if (isCI()) {
reports.junitXml.includeSystemOutLog = false
reports.junitXml.includeSystemErrLog = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ fun enabledChecks() = listOf(
"MissingDefault",
"MixedArrayDimensions",
"MissingDefault",
"MissingRuntimeRetention",
"MockitoDoSetup",
"MutableGuiceModule",
"NoAllocation",
Expand Down
2 changes: 1 addition & 1 deletion simulator/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ tasks.withType<JavaExec>().configureEach {
classpath(sourceSets["main"].runtimeClasspath)
systemProperties(caffeineSystemProperties())
outputs.upToDateWhen { false }
jvmArgs(javaExecJvmArgs())
jvmArgs(defaultJvmArgs())
}

eclipse.classpath.file.beforeMerged {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@
*/
package com.github.benmanes.caffeine.cache.simulator.admission;

import static java.util.Locale.US;

import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.UnaryOperator;

import com.github.benmanes.caffeine.cache.simulator.BasicSettings;
import com.github.benmanes.caffeine.cache.simulator.admission.clairvoyant.Clairvoyant;
import com.github.benmanes.caffeine.cache.simulator.policy.PolicyStats;
import com.google.common.base.Enums;
import com.typesafe.config.Config;

/**
Expand All @@ -29,15 +32,16 @@
*/
@SuppressWarnings("ImmutableEnumChecker")
public enum Admission {
ALWAYS((config, policyStats) -> Admittor.always(), UnaryOperator.identity()),
TINYLFU(TinyLfu::new, name -> name + "_TinyLfu");
ALWAYS((config, policyStats) -> Admittor.always(), ""),
CLAIRVOYANT(Clairvoyant::new, "_Clairvoyant"),
TINYLFU(TinyLfu::new, "_TinyLfu");

private final BiFunction<Config, PolicyStats, Admittor> factory;
private final Function<String, String> formatter;
private final String suffix;

Admission(BiFunction<Config, PolicyStats, Admittor> factory, UnaryOperator<String> formatter) {
this.formatter = formatter;
Admission(BiFunction<Config, PolicyStats, Admittor> factory, String suffix) {
this.factory = factory;
this.suffix = suffix;
}

/**
Expand All @@ -48,11 +52,16 @@ public enum Admission {
* @return an admission policy
*/
public Admittor from(Config config, PolicyStats policyStats) {
if (this == TINYLFU) {
var override = new BasicSettings(config).tinyLfu().sketch().toUpperCase(US);
return Enums.getIfPresent(Admission.class, override).or(this)
.factory.apply(config, policyStats);
}
return factory.apply(config, policyStats);
}

/** Returns the policy's formatted name. */
public String format(String name) {
return formatter.apply(name);
return name + suffix;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
package com.github.benmanes.caffeine.cache.simulator.admission;

import static java.util.Locale.US;

import java.util.Random;

import com.github.benmanes.caffeine.cache.simulator.BasicSettings;
Expand Down Expand Up @@ -57,33 +59,6 @@ public TinyLfu(Config config, PolicyStats policyStats) {
}
}

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

public int frequency(long key) {
return sketch.frequency(key);
}
Expand All @@ -107,4 +82,26 @@ public boolean admit(long candidateKey, long victimKey) {
policyStats.recordRejection();
return false;
}

/** Returns the frequency histogram. */
private static Frequency makeSketch(BasicSettings settings) {
String type = settings.tinyLfu().sketch();
switch (type.toLowerCase(US)) {
case "count-min-4": {
String reset = settings.tinyLfu().countMin4().reset();
switch (reset.toLowerCase(US)) {
case "climber": return new ClimberResetCountMin4(settings.config());
case "periodic": return new PeriodicResetCountMin4(settings.config());
case "indicator": return new IndicatorResetCountMin4(settings.config());
case "incremental": return new IncrementalResetCountMin4(settings.config());
default: throw new IllegalStateException("Unknown reset type: " + reset);
}
}
case "tiny-table": return new TinyCacheAdapter(settings.config());
case "count-min-64": return new CountMin64TinyLfu(settings.config());
case "perfect-table": return new PerfectFrequency(settings.config());
case "random-table": return new RandomRemovalFrequencyTable(settings.config());
default: throw new IllegalStateException("Unknown sketch type: " + type);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright 2024 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.clairvoyant;

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

import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream;

import com.github.benmanes.caffeine.cache.simulator.BasicSettings;
import com.github.benmanes.caffeine.cache.simulator.admission.Admittor.KeyOnlyAdmittor;
import com.github.benmanes.caffeine.cache.simulator.policy.AccessEvent;
import com.github.benmanes.caffeine.cache.simulator.policy.PolicyStats;
import com.typesafe.config.Config;

import it.unimi.dsi.fastutil.ints.IntArrayFIFOQueue;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import it.unimi.dsi.fastutil.ints.IntPriorityQueue;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;

/**
* {@literal Bélády's} optimal page replacement policy applied as an admission policy by comparing
* the keys using their next access times.
*
* @author ben.manes@gmail.com (Ben Manes)
*/
public final class Clairvoyant implements KeyOnlyAdmittor {
private static final AtomicReference<Long2ObjectMap<IntList>> snapshot = new AtomicReference<>();

private final Long2ObjectMap<IntPriorityQueue> accessTimes;
private final PolicyStats policyStats;

public Clairvoyant(Config config, PolicyStats policyStats) {
if (snapshot.get() == null) {
snapshot.set(readAccessTimes(new BasicSettings(config)));
}
accessTimes = new Long2ObjectOpenHashMap<>(snapshot.get().size());
for (var entry : snapshot.get().long2ObjectEntrySet()) {
var times = new IntArrayFIFOQueue(entry.getValue().size());
accessTimes.put(entry.getLongKey(), times);
entry.getValue().forEach(times::enqueue);
}
this.policyStats = policyStats;
}

@Override
public void record(long key) {
if (snapshot.get() != null) {
snapshot.set(null);
}

var times = accessTimes.get(key);
if (times == null) {
return;
}
times.dequeueInt();
if (times.isEmpty()) {
accessTimes.remove(key);
}
}

@Override
public boolean admit(long candidateKey, long victimKey) {
var candidateTime = nextAccessTime(candidateKey);
var victimTime = nextAccessTime(victimKey);
if (candidateTime > victimTime) {
policyStats.recordRejection();
return false;
}
policyStats.recordAdmission();
return true;
}

private int nextAccessTime(long key) {
var times = accessTimes.get(key);
return ((times == null) || times.isEmpty()) ? Integer.MAX_VALUE : times.firstInt();
}

private static Long2ObjectMap<IntList> readAccessTimes(BasicSettings settings) {
checkState(!settings.trace().isSynthetic(), "Synthetic traces cannot be predicted");
var accessTimes = new Long2ObjectOpenHashMap<IntList>();
var trace = settings.trace().traceFiles().format()
.readFiles(settings.trace().traceFiles().paths());
try (Stream<AccessEvent> events = trace.events()) {
int[] tick = { 0 };
events.forEach(event -> {
var times = accessTimes.get(event.key());
if (times == null) {
times = new IntArrayList();
accessTimes.put(event.key(), times);
}
times.add(++tick[0]);
});
}
return accessTimes;
}
}
Loading

0 comments on commit 98895ff

Please sign in to comment.