-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Introduce memoizer API for AnnotationMetadata #11970
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
yawkat
wants to merge
8
commits into
4.10.x
Choose a base branch
from
memoizer
base: 4.10.x
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
3713cec
Introduce memoizer API for AnnotationMetadata
yawkat c4f38fe
apply to AnnotationMetadataHierarchy too
yawkat ae0d66e
Add itable-less getter for testing
yawkat da3c1f9
flesh out API change a bit
yawkat c304761
CR
yawkat b3f80c0
Remove unnecessary acquire release
yawkat f307699
acquire release array copy
yawkat e6c3454
Use invokeExact
yawkat File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
123 changes: 123 additions & 0 deletions
123
benchmarks/src/jmh/java/io/micronaut/core/util/memo/MemoBenchmark.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
package io.micronaut.core.util.memo; | ||
|
||
import io.micronaut.core.annotation.AnnotationMetadata; | ||
import io.micronaut.core.annotation.Introspected; | ||
import io.micronaut.core.beans.BeanIntrospection; | ||
import io.micronaut.core.beans.BeanIntrospector; | ||
import io.micronaut.core.beans.BeanProperty; | ||
import org.openjdk.jmh.annotations.Benchmark; | ||
import org.openjdk.jmh.annotations.BenchmarkMode; | ||
import org.openjdk.jmh.annotations.Fork; | ||
import org.openjdk.jmh.annotations.Measurement; | ||
import org.openjdk.jmh.annotations.Mode; | ||
import org.openjdk.jmh.annotations.OutputTimeUnit; | ||
import org.openjdk.jmh.annotations.Param; | ||
import org.openjdk.jmh.annotations.Scope; | ||
import org.openjdk.jmh.annotations.Setup; | ||
import org.openjdk.jmh.annotations.State; | ||
import org.openjdk.jmh.annotations.Warmup; | ||
import org.openjdk.jmh.runner.Runner; | ||
import org.openjdk.jmh.runner.options.Options; | ||
import org.openjdk.jmh.runner.options.OptionsBuilder; | ||
|
||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.RetentionPolicy; | ||
import java.util.concurrent.TimeUnit; | ||
|
||
@Warmup(iterations = 5, time = 3, timeUnit = TimeUnit.SECONDS) | ||
@Measurement(iterations = 5, time = 3, timeUnit = TimeUnit.SECONDS) | ||
@OutputTimeUnit(TimeUnit.NANOSECONDS) | ||
@BenchmarkMode(Mode.AverageTime) | ||
@Fork(1) | ||
@State(Scope.Benchmark) | ||
public class MemoBenchmark { | ||
private static final MemoizedFlag<AnnotationMetadata> ANNOTATED_FLAG = | ||
AnnotationMetadata.MEMOIZER_NAMESPACE.newFlag(m -> m.hasAnnotation(Ann1.class)); | ||
|
||
@Param({"Bare", "Extra1", "Extra2"}) | ||
String type; | ||
@Param({"true", "false"}) | ||
boolean annotated; | ||
|
||
private BeanProperty<?, ?> property; | ||
|
||
@Setup | ||
public void setup() throws Throwable { | ||
BeanIntrospection<?> introspection = BeanIntrospector.SHARED.getIntrospection(Class.forName(MemoBenchmark.class.getName() + "$" + type)); | ||
property = introspection.getProperty(annotated ? "annotatedField" : "notAnnotatedField").orElseThrow(); | ||
|
||
if (memoized() != annotated) { | ||
throw new AssertionError(); | ||
} | ||
if (memoizedFallback() != annotated) { | ||
throw new AssertionError(); | ||
} | ||
if (direct() != annotated) { | ||
throw new AssertionError(); | ||
} | ||
} | ||
|
||
@Benchmark | ||
public boolean memoized() { | ||
return ANNOTATED_FLAG.get(property); | ||
} | ||
|
||
@Benchmark | ||
public boolean memoizedFallback() { | ||
// this is the default implementation of get() | ||
return ANNOTATED_FLAG.compute(property); | ||
} | ||
|
||
@Benchmark | ||
public boolean direct() { | ||
return property.hasAnnotation(Ann1.class); | ||
} | ||
|
||
public static void main(String[] args) throws Throwable { | ||
MemoBenchmark memoBenchmark = new MemoBenchmark(); | ||
memoBenchmark.type = "Bare"; | ||
memoBenchmark.annotated = true; | ||
memoBenchmark.setup(); | ||
memoBenchmark.memoizedFallback(); | ||
|
||
Options opt = new OptionsBuilder() | ||
.include(MemoBenchmark.class.getName() + ".*") | ||
//.addProfiler(LinuxPerfAsmProfiler.class) | ||
.build(); | ||
|
||
new Runner(opt).run(); | ||
} | ||
|
||
@Introspected | ||
record Bare( | ||
@Ann1 String annotatedField, | ||
String notAnnotatedField | ||
) { | ||
} | ||
|
||
@Introspected | ||
record Extra1( | ||
@Ann1 @Ann2 String annotatedField, | ||
@Ann2 String notAnnotatedField | ||
) { | ||
} | ||
|
||
@Introspected | ||
record Extra2( | ||
@Ann1 @Ann2 @Ann3 String annotatedField, | ||
@Ann2 @Ann3 String notAnnotatedField | ||
) { | ||
} | ||
|
||
@Retention(RetentionPolicy.RUNTIME) | ||
@interface Ann1 { | ||
} | ||
|
||
@Retention(RetentionPolicy.RUNTIME) | ||
@interface Ann2 { | ||
} | ||
|
||
@Retention(RetentionPolicy.RUNTIME) | ||
@interface Ann3 { | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
125 changes: 125 additions & 0 deletions
125
core/src/main/java/io/micronaut/core/util/memo/AbstractMemoizer.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
/* | ||
* Copyright 2017-2025 original authors | ||
* | ||
* 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 | ||
* | ||
* https://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 io.micronaut.core.util.memo; | ||
|
||
import io.micronaut.core.annotation.Internal; | ||
import io.micronaut.core.annotation.NonNull; | ||
import io.micronaut.core.util.ArrayUtils; | ||
|
||
import java.lang.invoke.MethodHandles; | ||
import java.lang.invoke.VarHandle; | ||
|
||
/** | ||
* Implementation of {@link Memoizer} with backing storage. | ||
* | ||
* @param <M> This memoizer type | ||
* @author Jonas Konrad | ||
* @since 4.10.0 | ||
*/ | ||
@Internal | ||
public abstract class AbstractMemoizer<M extends Memoizer<M>> implements Memoizer<M> { | ||
private static final Object NULL_SENTINEL = new Object(); | ||
private static final VarHandle ITEMS_ENTRY = MethodHandles.arrayElementVarHandle(Object[].class).withInvokeExactBehavior(); | ||
|
||
private Object[] items = ArrayUtils.EMPTY_OBJECT_ARRAY; | ||
private long flags; | ||
|
||
/** | ||
* Get the namespace that should be used for this memoizer. This is a method instead of a field | ||
* to save on memory. This namespace is only spot-checked on field population. | ||
* | ||
* @return The namespace | ||
*/ | ||
@NonNull | ||
@Internal | ||
protected abstract MemoizerNamespace<M> getMemoizerNamespace(); | ||
|
||
/** | ||
* Clear all memoized values, for use when this instance is modified and memoization may now | ||
* return a different value. This is a cheap operation. | ||
*/ | ||
@Internal | ||
protected final void clearMemoized() { | ||
items = ArrayUtils.EMPTY_OBJECT_ARRAY; | ||
flags = 0; | ||
} | ||
|
||
@SuppressWarnings("unchecked") | ||
final <R> R getMemoized(MemoizedReference<M, R> reference) { | ||
Object[] items = this.items; | ||
int index = reference.index; | ||
if (index >= items.length) { | ||
items = ensureCapacity(items, index); | ||
} | ||
Object item = ITEMS_ENTRY.getAcquire(items, index); | ||
if (item == null) { | ||
item = computeItem(reference, items, index); | ||
} | ||
if (item == NULL_SENTINEL) { | ||
item = null; | ||
} | ||
return (R) item; | ||
} | ||
|
||
private Object[] ensureCapacity(Object[] items, int accessedIndex) { | ||
Object[] newItems = new Object[accessedIndex + 1]; | ||
for (int i = 0; i < items.length; i++) { | ||
ITEMS_ENTRY.setRelease(newItems, i, ITEMS_ENTRY.getAcquire(items, i)); | ||
} | ||
this.items = newItems; | ||
return newItems; | ||
} | ||
|
||
@SuppressWarnings("unchecked") | ||
private Object computeItem(MemoizedReference<M, ?> reference, Object[] items, int index) { | ||
if (reference.namespace != getMemoizerNamespace()) { | ||
throw new IllegalStateException("Reference not in right namespace"); | ||
} | ||
Object item = reference.compute.apply((M) this); | ||
if (item == null) { | ||
item = NULL_SENTINEL; | ||
} | ||
ITEMS_ENTRY.setRelease(items, index, item); | ||
return item; | ||
} | ||
|
||
final boolean getMemoized(@NonNull MemoizedFlag<M> flag) { | ||
if (!(flag instanceof MemoizedFlag.InBitmask<M> ib)) { | ||
return getMemoizedViaReference(flag); | ||
} | ||
long flags = this.flags; | ||
if ((flags & ib.maskIsSet) == 0) { | ||
flags = computeFlag(flags, ib); | ||
} | ||
return (flags & ib.maskValue) != 0; | ||
} | ||
|
||
private boolean getMemoizedViaReference(MemoizedFlag<M> flag) { | ||
return getMemoized(((MemoizedFlag.InReference<M>) flag).reference); | ||
} | ||
|
||
@SuppressWarnings("unchecked") | ||
private long computeFlag(long flags, MemoizedFlag.InBitmask<M> ib) { | ||
flags |= ib.maskIsSet; | ||
if (ib.compute.test((M) this)) { | ||
flags |= ib.maskValue; | ||
} else { | ||
flags &= ~ib.maskValue; | ||
} | ||
this.flags = flags; | ||
return flags; | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add javadoc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mutable data structure in a static field?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes it's mutable, that's why it's important to only create a limited number of
MemoizedReference
s.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the concern is that users could fiddle with this mutable static fields and create weird bugs