Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
0e8a854
Initial commit
minborg May 5, 2025
5ecee27
Rename classes
minborg May 5, 2025
6c95aac
Add @Stable for URI and add benchmark
minborg May 5, 2025
2bc67cc
Add examples to doc and fix tests
minborg May 6, 2025
2548d7f
Add copyright header and revert unintended change
minborg May 6, 2025
2fb0cb3
Move raw factories into inner class Raw
minborg May 6, 2025
94b4023
Add unhecked call test
minborg May 6, 2025
a3546a6
Document field alignment assumption
minborg May 6, 2025
33e1779
Address comments
minborg May 6, 2025
52e96c5
Fix raw long updater under 32-bit mode
minborg May 6, 2025
83ce9ac
Add a method handle based field updater
minborg May 7, 2025
0353ff9
Revert changes in public classes
minborg May 7, 2025
4a42b27
Reformat
minborg May 7, 2025
94a9a4e
Merge branch 'master' into stable-updaters
minborg May 7, 2025
6342fbd
Add low level variants
minborg May 7, 2025
a01ba9a
Add convenience methods and documentations
minborg May 7, 2025
de8e238
Reformat
minborg May 7, 2025
e2a2d7b
Add lazy CallSite methods
minborg May 8, 2025
e928591
Revert changes in stable classes
minborg May 8, 2025
1628b26
Add composition of functions and MHs
minborg May 8, 2025
abc0a3b
Merge branch 'master' into stable-updaters
minborg May 9, 2025
ce1b832
Remove unused factories and add comment
minborg May 9, 2025
cbcf13b
Revert unintended change
minborg May 9, 2025
9a58db6
Revert unintended change
minborg May 9, 2025
f6de843
Merge branch 'master' into stable-updaters
minborg May 13, 2025
119cbcf
Wip
minborg May 14, 2025
2df0807
Fix docs
minborg May 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions make/test/BuildMicrobenchmark.gmk
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ $(eval $(call SetupJavaCompilation, BUILD_JDK_MICROBENCHMARK, \
--add-exports java.base/jdk.internal.classfile.impl=ALL-UNNAMED \
--add-exports java.base/jdk.internal.event=ALL-UNNAMED \
--add-exports java.base/jdk.internal.foreign=ALL-UNNAMED \
--add-exports java.base/jdk.internal.lang.stable=ALL-UNNAMED \
--add-exports java.base/jdk.internal.misc=ALL-UNNAMED \
--add-exports java.base/jdk.internal.util=ALL-UNNAMED \
--add-exports java.base/jdk.internal.vm=ALL-UNNAMED \
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,365 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/

package jdk.internal.lang.stable;

import jdk.internal.invoke.MhUtil;
import jdk.internal.util.Architecture;
import jdk.internal.vm.annotation.ForceInline;

import java.lang.invoke.CallSite;
import java.lang.invoke.ConstantCallSite;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.invoke.VarHandle;
import java.util.List;
import java.util.Objects;

import static jdk.internal.lang.stable.StableFieldUpdaterGenerator.*;

/**
* Stable field updaters.
* <p>
* This class allows, for example, effortless conversion of immutable classes to use lazy
* {@link Object#hashCode()}.
* <p>
* Here is an example of how to convert
* <p>
* {@snippet lang = java:
* public final class Foo {
*
* private final Bar bar;
* private final Baz baz;
*
* public Foo(Bar bar, Baz baz) {
* this.bar = bar;
* this.baz = baz;
* }
*
* @Override
* public boolean equals(Object o) {
* return o instanceof Foo that
* && Objects.equals(this.bar, that.bar)
* && Objects.equals(this.baz, that.baz);
* }
*
* @Override
* public int hashCode() {
* return Objects.hash(bar, baz);
* }
* }
*} to use {@code @Stable} lazy hashing:
* <p>
* {@snippet lang = java:
* public final class LazyFoo {
*
* private final Bar bar;
* private final Baz baz;
*
* private static final MethodHandle HASH_UPDATER;
*
* static {
* MethodHandles.Lookup lookup = MethodHandles.lookup();
* try {
* VarHandle accessor = lookup.findVarHandle(LazyFoo.class, "hash", int.class);
* MethodHandle underlying = lookup.findStatic(LazyFoo.class, "hashCodeFor", MethodType.methodType(int.class, LazyFoo.class));
* HASH_UPDATER = StableFieldUpdater.atMostOnce(accessor, underlying);
* } catch (ReflectiveOperationException e) {
* throw new InternalError(e);
* }
* }
*
* @Stable
* private int hash;
*
* public LazyFoo(Bar bar, Baz baz) {
* this.bar = bar;
* this.baz = baz;
* }
*
* @Override
* public boolean equals(Object o) {
* return o instanceof Foo that
* && Objects.equals(this.bar, that.bar)
* && Objects.equals(this.baz, that.baz);
* }
*
* @Override
* public int hashCode() {
* try {
* return (int) HASH_UPDATER.invokeExact(this);
* } catch (Throwable e) {
* throw new RuntimeException(e);
* }
* }
*
* private static int hashCodeFor(LazyFoo foo) {
* return Objects.hash(foo.bar, foo.baz);
* }
* }
*}
* <p>
* If the underlying hash lamba returns zero, the hash code will be {@code 0}. In such
* cases, {@link @Stable} fields cannot be constant-folded.
* <p>
* In cases where the entire range of hash codes are strictly specified (as it is for
* {@code String}), a {@code long} field can be used instead, and a value of
* {@code 1 << 32} can be used as a token for zero (as the lower 32 bits are zero) and
* then just cast to an {@code int} as shown in this example:
*
* {@snippet lang = java:
* public final class LazySpecifiedFoo {
*
* private final Bar bar;
* private final Baz baz;
*
* private static final MethodHandle HASH_UPDATER;
*
* static {
* MethodHandles.Lookup lookup = MethodHandles.lookup();
* try {
* VarHandle accessor = lookup.findVarHandle(LazySpecifiedFoo.class, "hash", long.class);
* MethodHandle underlying = lookup.findStatic(LazySpecifiedFoo.class, "hashCodeFor", MethodType.methodType(long.class, LazySpecifiedFoo.class));
*
* // Replaces zero with 2^32. Bits 32-63 will then be masked away by the
* // `hashCode()` method using an (int) cast.
* underlying = StableFieldUpdater.replaceLongZero(underlying, 1L << 32);
*
* HASH_UPDATER = StableFieldUpdater.atMostOnce(accessor, underlying);
* } catch (ReflectiveOperationException e) {
* throw new InternalError(e);
* }
* }
*
* @Stable
* private long hash;
*
* public LazySpecifiedFoo(Bar bar, Baz baz) {
* this.bar = bar;
* this.baz = baz;
* }
*
* @Override
* public boolean equals(Object o) {
* return o instanceof Foo that
* && Objects.equals(this.bar, that.bar)
* && Objects.equals(this.baz, that.baz);
* }
*
* @Override
* public int hashCode() {
* try {
* return (int) (long) HASH_UPDATER.invokeExact(this);
* } catch (Throwable e) {
* throw new RuntimeException(e);
* }
* }
*
* private static long hashCodeFor(LazySpecifiedFoo foo) {
* return Objects.hash(foo.bar, foo.baz);
* }
* }
*}
* The example above also features a static method {@code hashCodeFor()} that acts as
* the underlying hash function. This method can reside in another class.
* <p>
*
* The provided {@code underlying} function must not recurse or an IllegalStateException
* will be thrown.
* <p>
* If a reference value of {@code null} is used as a parameter in any of the methods
* in this class, a {@link NullPointerException} is thrown.
*/
public final class StableFieldUpdater {

private StableFieldUpdater() {}

private static final MethodHandles.Lookup LOCAL_LOOKUP = MethodHandles.lookup();

/**
* {@return a function that lazily sets the field accessible via the provided
* {@code accessor} by invoking the provided {@code underlying} function
* if the field has its default value (e.g. zero). Otherwise, the
* returned function returns the set field value}
* <p>
Comment on lines +203 to +207

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inline form of @return is not indented elsewhere in the JDK:

Suggested change
* {@return a function that lazily sets the field accessible via the provided
* {@code accessor} by invoking the provided {@code underlying} function
* if the field has its default value (e.g. zero). Otherwise, the
* returned function returns the set field value}
* <p>
* {@return a function that lazily sets the field accessible via the provided
* {@code accessor} by invoking the provided {@code underlying} function
* if the field has its default value (e.g. zero). Otherwise, the returned
* function returns the set field value}

*
* @param accessor used to access a field
* @param underlying function which is invoked if a field has its
* default value.
* @throws NullPointerException if any of the provided parameters are {@code null}
* @throws IllegalArgumentException if the accessor's {@linkplain VarHandle#varType()}
* is not equal to the return type of the
* underlying's {@linkplain MethodHandle#type()}
* @throws IllegalArgumentException if the provided {@code underlying} function does
* take exactly one parameter of a reference type.
Comment on lines +216 to +217

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @throws IllegalArgumentException if the provided {@code underlying} function does
* take exactly one parameter of a reference type.
* @throws IllegalArgumentException if the provided {@code underlying} function does
* not take the same parameter types as the
* {@code accessor} var handle takes coordinate types.

*/
public static MethodHandle atMostOnce(VarHandle accessor,
MethodHandle underlying) {
// Implicit null check
final var accessorVarType = accessor.varType();
final var accessorCoordinateTypes = accessor.coordinateTypes();
final var underlyingType = underlying.type();

if (accessorVarType != underlyingType.returnType()) {
throw new IllegalArgumentException("Return type mismatch: accessor: " + accessor + ", underlying: " + underlying);
}

if (!accessorCoordinateTypes.equals(underlyingType.parameterList())) {
throw new IllegalArgumentException("Parameter type mismatch: accessor: " + accessor + ", underlying: " + underlying);
}

if (!accessor.isAccessModeSupported(VarHandle.AccessMode.SET)) {
throw new IllegalArgumentException("The accessor is read only: " + accessor);
}

// Allow `invokeExact()` of the `apply(Object)` method
final MethodHandle adaptedUnderlying = underlyingType.parameterType(0).equals(Object.class)
|| underlyingType.parameterType(0).isArray()
? underlying
: underlying.asType(underlyingType.changeParameterType(0, Object.class));

Comment on lines +238 to +243

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This procedure is repeated in StableFieldUpdaterGenerator​::handle(…):

Suggested change
// Allow `invokeExact()` of the `apply(Object)` method
final MethodHandle adaptedUnderlying = underlyingType.parameterType(0).equals(Object.class)
|| underlyingType.parameterType(0).isArray()
? underlying
: underlying.asType(underlyingType.changeParameterType(0, Object.class));

final MethodHandle initialAccessor = accessor.toMethodHandle(initialAccessMode(accessorVarType));

return StableFieldUpdaterGenerator.handle(accessor, initialAccessor, underlying);
}

private static VarHandle.AccessMode initialAccessMode(Class<?> varType) {
if (varType.isPrimitive()) {
if (!Architecture.is64bit() && (varType.equals(long.class) || varType.equals(double.class))) {
// JLS (24) 17.4 states that 64-bit fields tear under plain memory semantics.
// Opaque semantics provides "Bitwise Atomicity" thus avoiding tearing.
return VarHandle.AccessMode.GET_OPAQUE;
} else {
// Reordering does not affect primitive values
// Todo: This does not establish a happens-before relation.
// Plain semantics suffice here as we are not dealing with a reference (for
// a reference, the internal state initialization can be reordered with
// other store ops).
return VarHandle.AccessMode.GET;
}
} else {
// Acquire semantics is needed for reference variables to prevent the internal
// state initialization can be reordered with other store ops.
return VarHandle.AccessMode.GET_ACQUIRE;
}
}

/**
* {@return a method handle that will replace any zero value returned by the provided
* {@code underlying} handle with the provided {@code zeroReplacement}}
* @param underlying function to filter return values from
* @param zeroReplacement to replace any zero values returned by the {@code underlying}
* method handle.
*/
public static MethodHandle replaceIntZero(MethodHandle underlying, int zeroReplacement) {

final class Holder {
private static final MethodHandle RETURN_FILTER =
MhUtil.findStatic(LOCAL_LOOKUP, "replaceZero", MethodType.methodType(int.class, int.class, int.class));
}
check(underlying, int.class);
return MethodHandles.filterReturnValue(underlying,
MethodHandles.insertArguments(Holder.RETURN_FILTER, 1, zeroReplacement));
}

/**
* {@return a method handle that will replace any zero value returned by the provided
* {@code underlying} handle with the provided {@code zeroReplacement}}
* @param underlying function to filter return values from
* @param zeroReplacement to replace any zero values returned by the {@code underlying}
* method handle.
*/
public static MethodHandle replaceLongZero(MethodHandle underlying, long zeroReplacement) {

final class Holder {
private static final MethodHandle RETURN_FILTER =
MhUtil.findStatic(LOCAL_LOOKUP, "replaceZero", MethodType.methodType(long.class, long.class, long.class));
}

check(underlying, long.class);
return MethodHandles.filterReturnValue(underlying,
MethodHandles.insertArguments(Holder.RETURN_FILTER, 1, zeroReplacement));
}

/**
* {@return a method handle that will replace any null value returned by the provided
* {@code underlying} handle with the provided {@code nullReplacement}}
* @param underlying function to filter return values from
* @param nullReplacement to replace any zero values returned by the {@code underlying}
* method handle.
*/
public static MethodHandle replaceReferenceNull(MethodHandle underlying, Object nullReplacement) {

final class Holder {
private static final MethodHandle RETURN_FILTER =
MhUtil.findStatic(LOCAL_LOOKUP, "replaceNull", MethodType.methodType(Object.class, Object.class, Object.class));
}
check(underlying, Object.class);
return MethodHandles.filterReturnValue(underlying,
MethodHandles.insertArguments(Holder.RETURN_FILTER, 1, nullReplacement));
}

// Used reflectively
@ForceInline
private static int replaceZero(int value, int zeroReplacement) {
return value == 0 ? zeroReplacement : value;
}

// Used reflectively
@ForceInline
private static long replaceZero(long value, long zeroReplacement) {
return value == 0 ? zeroReplacement : value;
}

// Used reflectively
// Cannot reuse `Objects::requireNonNullElse` as it prohibits `null` replacements
@ForceInline
private static Object replaceNull(Object value, Object nullReplacement) {
return value == null ? nullReplacement : value;
}

public static CallSite lazyAtMostOnce(MethodHandles.Lookup lookup,
String unused,
VarHandle accessor,
MethodHandle underlying) {
Comment on lines +344 to +347

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a valid bootstrap method signature, as that needs a MethodType for an invokedynamic or Class<?> for a dynamic constant:

Suggested change
public static CallSite lazyAtMostOnce(MethodHandles.Lookup lookup,
String unused,
VarHandle accessor,
MethodHandle underlying) {
public static CallSite lazyAtMostOnce(MethodHandles.Lookup lookup,
String unused,
MethodType type,
VarHandle accessor,
MethodHandle underlying) {

Objects.requireNonNull(accessor);
Objects.requireNonNull(underlying);
var handle = MhUtil.findStatic(LOCAL_LOOKUP,
"atMostOnce", MethodType.methodType(MethodHandle.class, VarHandle.class, MethodHandle.class));
return new ConstantCallSite(MethodHandles.insertArguments(handle, 0, accessor, underlying));
Comment on lines +350 to +352

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes no sense as a bootstrap for a dynamic call site, as that’ll simply be equivalent to a call to atMostOnce.

I presume thou meant something like the following, which would actually create the stable field updater for use at the call site:

Suggested change
var handle = MhUtil.findStatic(LOCAL_LOOKUP,
"atMostOnce", MethodType.methodType(MethodHandle.class, VarHandle.class, MethodHandle.class));
return new ConstantCallSite(MethodHandles.insertArguments(handle, 0, accessor, underlying));
return new ConstantCallSite(atMostOnce(accessor, underlying).asType(type));

or by using the createTargetHook of ConstantCallSite:

Suggested change
var handle = MhUtil.findStatic(LOCAL_LOOKUP,
"atMostOnce", MethodType.methodType(MethodHandle.class, VarHandle.class, MethodHandle.class));
return new ConstantCallSite(MethodHandles.insertArguments(handle, 0, accessor, underlying));
var handle = MhUtil.findStatic(LOCAL_LOOKUP,
"atMostOnce", MethodType.methodType(MethodHandle.class, VarHandle.class, MethodHandle.class));
return new ConstantCallSite(type, MethodHandles.dropArguments(
MethodHandles.insertArguments(handle, 0, accessor, underlying),
0, ConstantCallSite.class));

}

// Static support functions

private static void check(MethodHandle underlying, Class<?> returnType) {
// Implicit null check
final var underlyingType = underlying.type();
if (underlyingType.returnType() != returnType || underlyingType.parameterCount() != 1) {
throw new IllegalArgumentException("Illegal underlying function: " + underlying);
}
}

}
Loading