Skip to content

Commit

Permalink
Cache MethodHandles in LocalCacheFactory and NodeFactory (#905)
Browse files Browse the repository at this point in the history
* Cache constructor MethodHandles in factories

Update LocalCacheFactory.java

* Cache constructor MethodHandles in factories

use invokeExact and findClass

* Cache constructor MethodHandles in factories

use var

* Cache BoundedLocalCache and Node factory instances

* Cache LocalCacheFactory and NodeFactory instances

added slow path for our AOT friends
  • Loading branch information
yuzawa-san committed Apr 29, 2023
1 parent 2349de9 commit 3b1fae4
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import java.lang.invoke.VarHandle;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;

import org.checkerframework.checker.nullness.qual.Nullable;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.ParameterSpec;
import com.squareup.javapoet.ParameterizedTypeName;
Expand Down Expand Up @@ -82,7 +82,7 @@ public final class Specifications {
public static final ParameterizedTypeName ASYNC_CACHE_LOADER = ParameterizedTypeName.get(
ClassName.get(PACKAGE_NAME, "AsyncCacheLoader"), TypeVariableName.get("? super K"), vTypeVar);
public static final ParameterSpec ASYNC_CACHE_LOADER_PARAM =
ParameterSpec.builder(ASYNC_CACHE_LOADER, "cacheLoader").build();
ParameterSpec.builder(ASYNC_CACHE_LOADER, "cacheLoader").addAnnotation(Nullable.class).build();

public static final TypeName REMOVAL_LISTENER = ParameterizedTypeName.get(
ClassName.get(PACKAGE_NAME, "RemovalListener"), kTypeVar, vTypeVar);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@
package com.github.benmanes.caffeine.cache.local;

import static com.github.benmanes.caffeine.cache.Specifications.ASYNC_CACHE_LOADER_PARAM;
import static com.github.benmanes.caffeine.cache.Specifications.LOCAL_CACHE_FACTORY;
import static com.github.benmanes.caffeine.cache.Specifications.BOUNDED_LOCAL_CACHE;
import static com.github.benmanes.caffeine.cache.Specifications.BUILDER_PARAM;
import javax.lang.model.element.Modifier;
import com.squareup.javapoet.FieldSpec;

/**
* Adds the constructor to the cache.
Expand All @@ -44,5 +47,8 @@ protected void execute() {
} else {
context.constructor.addStatement("super(builder, cacheLoader, async)");
}
context.cache
.addField(FieldSpec.builder(LOCAL_CACHE_FACTORY, "FACTORY", Modifier.STATIC, Modifier.FINAL)
.initializer("$N::new", context.className).build());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
*/
public final class LocalCacheContext {
public final boolean isFinal;
public final String className;
public final TypeName superClass;
public final TypeSpec.Builder cache;
public final MethodSpec.Builder constructor;
Expand All @@ -42,6 +43,7 @@ public final class LocalCacheContext {
public LocalCacheContext(TypeName superClass, String className, boolean isFinal,
Set<Feature> parentFeatures, Set<Feature> generateFeatures) {
this.isFinal = isFinal;
this.className = className;
this.superClass = superClass;
this.suppressedWarnings = new TreeSet<>();
this.cache = TypeSpec.classBuilder(className);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@
*/
package com.github.benmanes.caffeine;

import java.lang.invoke.LambdaMetafactory;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Constructor;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.infra.Blackhole;

/**
* @author ben.manes@gmail.com (Ben Manes)
Expand All @@ -34,35 +35,46 @@ public class FactoryBenchmark {
private final MethodHandleFactory methodHandleFactory = new MethodHandleFactory();

@Benchmark
public Alpha direct() {
return new Alpha();
public void direct(Blackhole blackhole) {
blackhole.consume(new Alpha());
}

@Benchmark
public Alpha methodHandle_invoke() {
return methodHandleFactory.invoke();
public void methodHandle_invoke(Blackhole blackhole) {
blackhole.consume(methodHandleFactory.invoke());
}

@Benchmark
public Alpha methodHandle_invokeExact() {
return methodHandleFactory.invokeExact();
public void methodHandle_invokeExact(Blackhole blackhole) {
blackhole.consume(methodHandleFactory.invokeExact());
}

@Benchmark
public Alpha reflection() {
return reflectionFactory.newInstance();
public void methodHandle_lambda(Blackhole blackhole) {
blackhole.consume(methodHandleFactory.lambda());
}

@Benchmark
public void reflection(Blackhole blackhole) {
blackhole.consume(reflectionFactory.newInstance());
}

static final class MethodHandleFactory {
private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
private static final MethodType METHOD_TYPE = MethodType.methodType(void.class);

private final MethodHandle methodHandle;
private final AlphaConstructor lambda;

MethodHandleFactory() {
try {
methodHandle = LOOKUP.findConstructor(Alpha.class, METHOD_TYPE);
} catch (NoSuchMethodException | IllegalAccessException e) {
lambda =
(AlphaConstructor) LambdaMetafactory
.metafactory(LOOKUP, "construct", MethodType.methodType(AlphaConstructor.class),
methodHandle.type(), methodHandle, methodHandle.type())
.getTarget().invokeExact();
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
Expand All @@ -82,6 +94,10 @@ Alpha invokeExact() {
throw new RuntimeException(e);
}
}

Alpha lambda() {
return lambda.construct();
}
}

static final class ReflectionFactory {
Expand All @@ -107,4 +123,8 @@ Alpha newInstance() {
static final class Alpha {
public Alpha() {}
}

private interface AlphaConstructor {
Alpha construct();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,25 @@
*/
package com.github.benmanes.caffeine.cache;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.checkerframework.checker.nullness.qual.Nullable;

/**
* A factory for caches optimized for a particular configuration.
*
* @author ben.manes@gmail.com (Ben Manes)
*/
final class LocalCacheFactory {
private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
private static final MethodType FACTORY = MethodType.methodType(
interface LocalCacheFactory {
MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
MethodType FACTORY = MethodType.methodType(
void.class, Caffeine.class, AsyncCacheLoader.class, boolean.class);
Map<String, LocalCacheFactory> FACTORIES = new ConcurrentHashMap<>();

private LocalCacheFactory() {}
<K, V> BoundedLocalCache<K, V> newInstance(Caffeine<K, V> builder,
@Nullable AsyncCacheLoader<? super K, V> cacheLoader, boolean async);

/** Returns a cache optimized for this configuration. */
static <K, V> BoundedLocalCache<K, V> newBoundedLocalCache(Caffeine<K, V> builder,
Expand All @@ -41,7 +43,7 @@ static <K, V> BoundedLocalCache<K, V> newBoundedLocalCache(Caffeine<K, V> builde
}

static String getClassName(Caffeine<?, ?> builder) {
var className = new StringBuilder(LocalCacheFactory.class.getPackageName()).append('.');
var className = new StringBuilder();
if (builder.isStrongKeys()) {
className.append('S');
} else {
Expand Down Expand Up @@ -80,10 +82,38 @@ static String getClassName(Caffeine<?, ?> builder) {

static <K, V> BoundedLocalCache<K, V> loadFactory(Caffeine<K, V> builder,
@Nullable AsyncCacheLoader<? super K, V> cacheLoader, boolean async, String className) {
var factory = FACTORIES.get(className);
if (factory == null) {
factory = FACTORIES.computeIfAbsent(className, LocalCacheFactory::newFactory);
}
return factory.newInstance(builder, cacheLoader, async);
}

static LocalCacheFactory newFactory(String className) {
try {
Class<?> clazz = Class.forName(className);
MethodHandle handle = LOOKUP.findConstructor(clazz, FACTORY);
return (BoundedLocalCache<K, V>) handle.invoke(builder, cacheLoader, async);
var clazz = LOOKUP.findClass(LocalCacheFactory.class.getPackageName() + "." + className);
try {
// Fast path
return (LocalCacheFactory) LOOKUP
.findStaticVarHandle(clazz, "FACTORY", LocalCacheFactory.class).get();
} catch (NoSuchFieldException e) {
// Slow path when native hints are missing the field, but may have the constructor.
var constructor = LOOKUP.findConstructor(clazz, FACTORY);
var methodHandle =
constructor.asType(constructor.type().changeReturnType(BoundedLocalCache.class));
return new LocalCacheFactory() {
@Override
public <K, V> BoundedLocalCache<K, V> newInstance(Caffeine<K, V> builder,
@Nullable AsyncCacheLoader<? super K, V> cacheLoader, boolean async) {
try {
return (BoundedLocalCache<K, V>) methodHandle.invokeExact(builder, cacheLoader,

Check warning on line 109 in caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalCacheFactory.java

View workflow job for this annotation

GitHub Actions / qodana

Unchecked warning

Unchecked cast: 'java.lang.Object' to 'com.github.benmanes.caffeine.cache.BoundedLocalCache'
async);
} catch (Throwable t) {
throw new IllegalStateException(className, t);
}
}
};
}
} catch (RuntimeException | Error e) {
throw e;
} catch (Throwable t) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@
*/
package com.github.benmanes.caffeine.cache;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.ref.ReferenceQueue;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import com.github.benmanes.caffeine.cache.References.LookupKeyReference;
import com.github.benmanes.caffeine.cache.References.WeakKeyReference;
Expand All @@ -31,6 +32,7 @@
interface NodeFactory<K, V> {
MethodType FACTORY = MethodType.methodType(void.class);
MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
Map<String, NodeFactory<Object, Object>> FACTORIES = new ConcurrentHashMap<>();

RetiredStrongKey RETIRED_STRONG_KEY = new RetiredStrongKey();
RetiredWeakKey RETIRED_WEAK_KEY = new RetiredWeakKey();
Expand Down Expand Up @@ -87,7 +89,7 @@ static <K, V> NodeFactory<K, V> newFactory(Caffeine<K, V> builder, boolean isAsy
}

static String getClassName(Caffeine<?, ?> builder, boolean isAsync) {
var className = new StringBuilder(Node.class.getPackageName()).append('.');
var className = new StringBuilder();
if (builder.isStrongKeys()) {
className.append('P');
} else {
Expand Down Expand Up @@ -131,11 +133,21 @@ static String getClassName(Caffeine<?, ?> builder, boolean isAsync) {
return className.toString();
}

@SuppressWarnings("unchecked")
static <K, V> NodeFactory<K, V> loadFactory(String className) {
var factory = FACTORIES.get(className);
if (factory == null) {
factory = FACTORIES.computeIfAbsent(className, NodeFactory::newFactory);
}
return (NodeFactory<K, V>) factory;
}

static NodeFactory<Object, Object> newFactory(String className) {
try {
Class<?> clazz = Class.forName(className);
MethodHandle handle = LOOKUP.findConstructor(clazz, FACTORY);
return (NodeFactory<K, V>) handle.invoke();
var clazz = LOOKUP.findClass(Node.class.getPackageName() + "." + className);
var constructor = LOOKUP.findConstructor(clazz, FACTORY);
return (NodeFactory<Object, Object>) constructor

Check warning on line 149 in caffeine/src/main/java/com/github/benmanes/caffeine/cache/NodeFactory.java

View workflow job for this annotation

GitHub Actions / qodana

Unchecked warning

Unchecked cast: 'java.lang.Object' to 'com.github.benmanes.caffeine.cache.NodeFactory'
.asType(constructor.type().changeReturnType(NodeFactory.class)).invokeExact();
} catch (RuntimeException | Error e) {
throw e;
} catch (Throwable t) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
Expand Down

0 comments on commit 3b1fae4

Please sign in to comment.