diff --git a/make/test/BuildMicrobenchmark.gmk b/make/test/BuildMicrobenchmark.gmk index 347ca44d25f39..8b20327a16455 100644 --- a/make/test/BuildMicrobenchmark.gmk +++ b/make/test/BuildMicrobenchmark.gmk @@ -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 \ diff --git a/src/java.base/share/classes/jdk/internal/lang/stable/StableFieldUpdater.java b/src/java.base/share/classes/jdk/internal/lang/stable/StableFieldUpdater.java new file mode 100644 index 0000000000000..d6bd5a06b5169 --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/lang/stable/StableFieldUpdater.java @@ -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. + *

+ * This class allows, for example, effortless conversion of immutable classes to use lazy + * {@link Object#hashCode()}. + *

+ * Here is an example of how to convert + *

+ * {@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: + *

+ * {@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); + * } + * } + *} + *

+ * If the underlying hash lamba returns zero, the hash code will be {@code 0}. In such + * cases, {@link @Stable} fields cannot be constant-folded. + *

+ * 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. + *

+ * + * The provided {@code underlying} function must not recurse or an IllegalStateException + * will be thrown. + *

+ * 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} + *

+ * + * @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. + */ + 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)); + + 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) { + 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)); + } + + // 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); + } + } + +} diff --git a/src/java.base/share/classes/jdk/internal/lang/stable/StableFieldUpdaterGenerator.java b/src/java.base/share/classes/jdk/internal/lang/stable/StableFieldUpdaterGenerator.java new file mode 100644 index 0000000000000..5371a1f874c1a --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/lang/stable/StableFieldUpdaterGenerator.java @@ -0,0 +1,347 @@ +/* + * 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.vm.annotation.ForceInline; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.invoke.VarHandle; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Stable field updater code generator. + */ +final class StableFieldUpdaterGenerator { + + private StableFieldUpdaterGenerator() {} + + private static final MethodHandles.Lookup LOCAL_LOOKUP = MethodHandles.lookup(); + + record Shape(Class varType, List> coordinateTypes){ + + static Shape of(VarHandle varHandle) { + final List> shapeCoordinateTypes = new ArrayList<>(varHandle.coordinateTypes().size()); + for (Class coordinate: varHandle.coordinateTypes()) { + shapeCoordinateTypes.add(toObjectIfReference(coordinate)); + } + return new Shape(toObjectIfReference(varHandle.varType()), shapeCoordinateTypes); + } + + static Class toObjectIfReference(Class type) { + // Todo: fix arrays of references + return type.isPrimitive() || type.isArray() + ? type + : Object.class; + } + + } + + // Used to hold hand-rolled and bespoke resolvers and shapeConstructors + record Maker(MethodHandle applyResolver, MethodHandle shapeConstructor) { + MethodHandle make(VarHandle accessor, MethodHandle initialAccessor, MethodHandle underlying) { + try { + return applyResolver + .bindTo(shapeConstructor + .invoke(accessor, initialAccessor, underlying)); + } catch (Throwable t) { + throw new InternalError(t); + } + } + } + + static MethodHandle handle(VarHandle accessor, + MethodHandle initialAccessor, + MethodHandle underlying) { + + final var underlyingType = underlying.type(); + + // Allow `invokeExact()` of the `apply(Object)` method + final var adaptedUnderlying = underlyingType.parameterType(0).equals(Object.class) + || underlyingType.parameterType(0).isArray() + ? underlying + : underlying.asType(underlyingType.changeParameterType(0, Object.class)); + + final var shape = Shape.of(accessor); + final var maker = MAKERS.computeIfAbsent(shape, + s -> { + // System.out.println("shape = " + s); + return bespokeMaker(accessor, initialAccessor, underlying); + }); + final var handle = maker.make(accessor, initialAccessor, adaptedUnderlying); + + return accessor.coordinateTypes().size() == 1 + ? handle.asType(MethodType.methodType(handle.type().returnType(), underlying.type().parameterType(0))) + : handle; + + } + + private static Maker bespokeMaker(VarHandle accessor, + MethodHandle initialAccessor, + MethodHandle underlying) { + + // Todo: Implement this using bytecode generation + throw new UnsupportedOperationException("No support for accessor: " + accessor + + ", initialAccessor: " + initialAccessor + ", underlying: " + underlying); + } + + + // Hand-rolled classes + + static final MethodHandle INT_ONCE_APPLY = + MhUtil.findVirtual(LOCAL_LOOKUP, IntOnce.class, "apply", + MethodType.methodType(int.class, Object.class)); + + record IntOnce(VarHandle accessor, + MethodHandle initialAccessor, + MethodHandle underlying) { + + // Used reflectively + @ForceInline + public int apply(Object t) { + try { + final int v = (int) initialAccessor.invoke(t); + return v == 0 ? applySlowPath(t) : v; + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + public int applySlowPath(Object t) { + preventReentry(this); + int v; + synchronized (this) { + v = (int) accessor.getAcquire(t); + if (v == 0) { + try { + v = (int) underlying.invokeExact(t); + } catch (Throwable e) { + throw new RuntimeException(e); + } + accessor.setRelease(t, v); + } + } + return v; + } + } + + static final MethodHandle LONG_ONCE_APPLY = + MhUtil.findVirtual(LOCAL_LOOKUP, LongOnce.class, "apply", + MethodType.methodType(long.class, Object.class)); + + record LongOnce(VarHandle accessor, + MethodHandle initialAccessor, + MethodHandle underlying) { + + // Used reflectively + @ForceInline + public long apply(Object t) { + try { + long v = (long) initialAccessor.invoke(t); + return v == 0 ? applySlowPath(t) : v; + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + public long applySlowPath(Object t) { + preventReentry(this); + long v; + synchronized (this) { + v = (long) accessor.getAcquire(t); + if (v == 0) { + try { + v = (long) underlying.invokeExact(t); + } catch (Throwable e) { + throw new RuntimeException(e); + } + accessor.setRelease(t, v); + } + } + return v; + } + + } + + static final MethodHandle REFERENCE_ONCE_APPLY = + MhUtil.findVirtual(LOCAL_LOOKUP, ReferenceOnce.class, "apply", + MethodType.methodType(Object.class, Object.class)); + + record ReferenceOnce(VarHandle accessor, + MethodHandle initialAccessor, + MethodHandle underlying) { + + // Used reflectively + @ForceInline + public Object apply(Object t) { + try { + final Object v = (Object) initialAccessor.invoke(t); + return v == null ? applySlowPath(t) : v; + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + public Object applySlowPath(Object t) { + preventReentry(this); + Object v; + synchronized (this) { + v = (Object) accessor.getAcquire(t); + if (v == null) { + try { + v = (int) underlying.invokeExact(t); + } catch (Throwable e) { + throw new RuntimeException(e); + } + accessor.setRelease(t, v); + } + } + return v; + } + } + + // Arrays + + static final MethodHandle INT_ARRAY_ONCE_APPLY = + MhUtil.findVirtual(LOCAL_LOOKUP, IntArrayOnce.class, "apply", + MethodType.methodType(int.class, int[].class, int.class)); + + record IntArrayOnce(VarHandle accessor, + MethodHandle initialAccessor, + MethodHandle underlying) { + + // Used reflectively + @ForceInline + public int apply(int[] array, int i) { + try { + final int v = (int) initialAccessor.invoke(array, i); + return v == 0 ? applySlowPath(array, i) : v; + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + public int applySlowPath(int[] array, int i) { + preventReentry(this); + int v; + synchronized (this) { + v = (int) accessor.getAcquire(array, i); + if (v == 0) { + try { + v = (int) underlying.invokeExact(array, i); + } catch (Throwable e) { + throw new RuntimeException(e); + } + accessor.setRelease(array, i, v); + } + } + return v; + } + } + + static final MethodHandle LONG_ARRAY_ONCE_APPLY = + MhUtil.findVirtual(LOCAL_LOOKUP, LongArrayOnce.class, "apply", + MethodType.methodType(long.class, long[].class, int.class)); + + record LongArrayOnce(VarHandle accessor, + MethodHandle initialAccessor, + MethodHandle underlying) { + + // Used reflectively + @ForceInline + public long apply(long[] array, int i) { + try { + final long v = (long) initialAccessor.invoke(array, i); + return v == 0 ? applySlowPath(array, i) : v; + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + public long applySlowPath(long[] array, int i) { + preventReentry(this); + long v; + synchronized (this) { + v = (long) accessor.getAcquire(array, i); + if (v == 0) { + try { + v = (long) underlying.invokeExact(array, i); + } catch (Throwable e) { + throw new RuntimeException(e); + } + accessor.setRelease(array, i, v); + } + } + return v; + } + } + + + // This method is not annotated with @ForceInline as it is always called + // in a slow path. + static void preventReentry(Object obj) { + if (Thread.holdsLock(obj)) { + throw new IllegalStateException("Recursive initialization of a stable value is illegal"); + } + } + + static Class componentTypeDeep(Class type) { + return type.isArray() + ? componentTypeDeep(type.componentType()) + : type; + } + + // Caching of Shapes + + private static final Map MAKERS = new ConcurrentHashMap<>(); + + static { + // Add hand-rolled classes + + // Scalars + MAKERS.put(new Shape(int.class, List.of(Object.class)), new Maker(INT_ONCE_APPLY, findMakerConstructor(IntOnce.class))); + MAKERS.put(new Shape(long.class, List.of(Object.class)), new Maker(LONG_ONCE_APPLY, findMakerConstructor(LongOnce.class))); + MAKERS.put(new Shape(Object.class, List.of(Object.class)), new Maker(REFERENCE_ONCE_APPLY, findMakerConstructor(ReferenceOnce.class))); + // Arrays + MAKERS.put(new Shape(int.class, List.of(int[].class, int.class)), new Maker(INT_ARRAY_ONCE_APPLY, findMakerConstructor(IntArrayOnce.class))); + MAKERS.put(new Shape(long.class, List.of(long[].class, int.class)), new Maker(LONG_ARRAY_ONCE_APPLY, findMakerConstructor(LongArrayOnce.class))); + } + + private static MethodHandle findMakerConstructor(Class type) { + try { + return LOCAL_LOOKUP.findConstructor(type, + MethodType.methodType(void.class, VarHandle.class, MethodHandle.class, MethodHandle.class)); + } catch (ReflectiveOperationException e) { + throw new InternalError(e); + } + } + +} diff --git a/test/jdk/java/lang/StableValue/StableFieldUpdaterExampleTest.java b/test/jdk/java/lang/StableValue/StableFieldUpdaterExampleTest.java new file mode 100644 index 0000000000000..b850321eaff68 --- /dev/null +++ b/test/jdk/java/lang/StableValue/StableFieldUpdaterExampleTest.java @@ -0,0 +1,232 @@ +/* + * 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. + * + * 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. + */ + +/* @test + * @summary Basic tests for StableFieldUpdaterExampleTest examples in javadoc + * @modules java.base/jdk.internal.lang.stable + * @modules java.base/jdk.internal.invoke + * @run junit StableFieldUpdaterExampleTest + */ + +import jdk.internal.invoke.MhUtil; +import jdk.internal.lang.stable.StableFieldUpdater; +import org.junit.jupiter.api.Test; + +import java.lang.Override; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.invoke.VarHandle; +import java.util.Objects; +import java.util.function.ToIntFunction; +import java.util.function.ToLongFunction; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * This test is to make sure the example in the documentation of + * {@linkplain StableFieldUpdater} remains correct and compilable. + */ +final class StableFieldUpdaterExampleTest { + + @interface Stable {} // No access to the real @Stable + + public static final class Bar extends Base { + public Bar(int hash) { + super(hash); + } + } + + public static final class Baz extends Base { + public Baz(int hash) { + super(hash); + } + } + + public static abstract class Base { + + final int hash; + + public Base(int hash) { + this.hash = hash; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof Base that && + this.getClass().equals(that.getClass()) && + this.hash == that.hash; + } + + @Override + public int hashCode() { + return hash; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "[hash=" + hash + ']'; + } + } + + + static // Intentionally in unblessed order to allow the example to look nice + + 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); + } + } + + static + + 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); + } + } + + static + + 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); + } + } + + + @Test + void lazy() { + var bar = new Bar(1); + var baz = new Baz(2); + + var foo = new LazyFoo(bar, baz); + + assertEquals(Objects.hash(1, 2), foo.hashCode()); + } + +} diff --git a/test/jdk/java/lang/StableValue/StableFieldUpdaterTest.java b/test/jdk/java/lang/StableValue/StableFieldUpdaterTest.java new file mode 100644 index 0000000000000..67cb299caaa5f --- /dev/null +++ b/test/jdk/java/lang/StableValue/StableFieldUpdaterTest.java @@ -0,0 +1,390 @@ +/* + * 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. + * + * 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. + */ + +/* @test + * @summary Basic tests for StableFieldUpdater implementations + * @modules java.base/jdk.internal.lang.stable + * @modules java.base/jdk.internal.invoke + * @run junit StableFieldUpdaterTest + */ + +import jdk.internal.invoke.MhUtil; +import jdk.internal.lang.stable.StableFieldUpdater; +import org.junit.jupiter.api.Named; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.lang.foreign.Arena; +import java.lang.foreign.MemoryLayout; +import java.lang.foreign.MemorySegment; +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.lang.invoke.WrongMethodTypeException; +import java.util.Objects; +import java.util.function.Function; +import java.util.function.IntFunction; +import java.util.function.ToIntFunction; +import java.util.function.ToLongFunction; +import java.util.stream.Stream; + +import static java.lang.foreign.ValueLayout.JAVA_INT; +import static org.junit.jupiter.api.Assertions.*; + +final class StableFieldUpdaterTest { + + private static final String STRING = "Abc"; + private static final int SIZE = 8; + + + private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); + + private static final VarHandle ACCESSOR; + private static final MethodHandle UNDERLYING; + private static final MethodHandle DOUBLE_UNDERLYING; + + static { + try { + ACCESSOR = LOOKUP.findVarHandle(Foo.class, "hash", int.class); + UNDERLYING = LOOKUP.findStatic(Foo.class, "hashCodeFor", MethodType.methodType(int.class, Foo.class)); + DOUBLE_UNDERLYING = LOOKUP.findStatic(StableFieldUpdaterTest.class, "doubleFrom", MethodType.methodType(double.class, Foo.class)); + } catch (ReflectiveOperationException e) { + throw new InternalError(e); + } + } + + @Test + void invariants() { + assertThrows(NullPointerException.class, () -> StableFieldUpdater.atMostOnce(null, UNDERLYING)); + assertThrows(NullPointerException.class, () -> StableFieldUpdater.atMostOnce(ACCESSOR, null)); + var xi = assertThrows(IllegalArgumentException.class, () -> StableFieldUpdater.atMostOnce(ACCESSOR, DOUBLE_UNDERLYING)); + assertEquals("Return type mismatch: accessor: VarHandle[varType=int, coord=[class StableFieldUpdaterTest$Foo]], underlying: MethodHandle(Foo)double", xi.getMessage()); + + assertThrows(NullPointerException.class, () -> StableFieldUpdater.replaceIntZero((MethodHandle) null, 1)); + assertThrows(NullPointerException.class, () -> StableFieldUpdater.replaceLongZero((MethodHandle) null, 1L)); + } + + @Test + void multiCoordinateIntArray() throws Throwable { + var accessor = MethodHandles.arrayElementVarHandle(int[].class); + var underlying = LOOKUP.findStatic(StableFieldUpdaterTest.class, "multiCoordinateMethod", + MethodType.methodType(int.class, int[].class, int.class)); + var atMostOnce = StableFieldUpdater.atMostOnce(accessor, underlying); + int index = 1; + int[] array = new int[SIZE]; + int val = (int) atMostOnce.invokeExact(array, index); + assertEquals(index, val); + } + + @Test + void multiCoordinateLongArray() throws Throwable { + var accessor = MethodHandles.arrayElementVarHandle(long[].class); + var underlying = LOOKUP.findStatic(StableFieldUpdaterTest.class, "multiCoordinateMethod", + MethodType.methodType(long.class, long[].class, int.class)); + var atMostOnce = StableFieldUpdater.atMostOnce(accessor, underlying); + int index = 1; + long[] array = new long[SIZE]; + long val = (long) atMostOnce.invokeExact(array, index); + assertEquals(index, val); + } + + @Test + void multiCoordinateSegment() throws Throwable { + var layout = MemoryLayout.sequenceLayout(SIZE, JAVA_INT); + var accessor = layout.varHandle(MemoryLayout.PathElement.sequenceElement()); + accessor = MethodHandles.insertCoordinates(accessor, 1, 0L); // zero offset + var underlying = LOOKUP.findStatic(StableFieldUpdaterTest.class, "multiCoordinateMethod", + MethodType.methodType(int.class, MemorySegment.class, long.class)); + var atMostOnce = StableFieldUpdater.atMostOnce(accessor, underlying); + long index = 1L; + try (var arena = Arena.ofConfined()) { + var segment = arena.allocate(layout); + int val = (int) atMostOnce.invokeExact(segment, index); + assertEquals(index, val); + } + } + + // Used reflectively: int[], int + static int multiCoordinateMethod(int[] array, int index) { + return index; + } + + // Used reflectively: + static long multiCoordinateMethod(long[] array, int index) { + return index; + } + + // Used reflectively + static int multiCoordinateMethod(MemorySegment segment, long index) { + return (int) index; + } + + @Test + void returnedHandleTypes() { + MethodHandle handle = StableFieldUpdater.atMostOnce(ACCESSOR, UNDERLYING); + assertEquals(int.class, handle.type().returnType()); + assertEquals(Foo.class, handle.type().parameterType(0)); + assertEquals(1, handle.type().parameterCount()); + } + + @Test + void foo() throws Throwable { + MethodHandle handle = StableFieldUpdater.atMostOnce(ACCESSOR, UNDERLYING); + Foo foo = new Foo(STRING); + int hash = (int) handle.invokeExact(foo); + assertEquals(STRING.hashCode(), hash); + } + + @ParameterizedTest + @MethodSource("fooConstructors") + void basic(Function ctor) { + final HasHashField foo = ctor.apply(STRING); + assertEquals(0L, foo.hash()); + int actual = foo.hashCode(); + assertEquals(STRING.hashCode(), actual); + assertEquals(actual, foo.hash()); + } + + @Test + void recordFoo() throws ReflectiveOperationException { + record RecordFoo(String string, int hash) { + + private int hashCodeFor() { + return string.hashCode(); + } + + } + var accessor = LOOKUP.findVarHandle(RecordFoo.class, "hash", int.class); + var underlying = LOOKUP.findVirtual(RecordFoo.class, "hashCodeFor", MethodType.methodType(int.class)); + + accessor.isAccessModeSupported(VarHandle.AccessMode.SET); + + // The field is `final` + var x = assertThrows(IllegalArgumentException.class, () -> StableFieldUpdater.atMostOnce(accessor, underlying)); + assertEquals("The accessor is read only: VarHandle[varType=int, coord=[class StableFieldUpdaterTest$1RecordFoo]]", x.getMessage()); + } + + @Test + void wrongReceiver() { + var handle = StableFieldUpdater.atMostOnce(ACCESSOR, UNDERLYING); + var wrongType = new InheritingFoo(STRING); + + assertThrows(WrongMethodTypeException.class, () -> { + int hash = (int) handle.invokeExact(wrongType); + }); + + assertThrows(ClassCastException.class, () -> { + int hash = (int) handle.invoke(wrongType); + }); + } + + @Test + void lazyAtMostOnce() throws Throwable { + var lookup = MethodHandles.lookup(); + CallSite callSite = StableFieldUpdater.lazyAtMostOnce(lookup, "", ACCESSOR, UNDERLYING); + + MethodHandle hasher = (MethodHandle) callSite.getTarget().invoke(); + + var foo = new Foo(STRING); + int hash = (int) hasher.invoke(foo); + assertEquals(STRING.hashCode(), hash); + } + + @Test + void replaceIntZeroHandle() throws Throwable { + int zeroReplacement = -1; + MethodHandle underlying = MethodHandles.identity(int.class); + var mod = StableFieldUpdater.replaceIntZero(underlying, zeroReplacement); + assertEquals(1, (int) mod.invoke(1)); + assertEquals(zeroReplacement, (int) mod.invoke(0)); + } + + @Test + void replaceLongZeroHandle() throws Throwable { + long zeroReplacement = -1; + MethodHandle underlying = MethodHandles.identity(long.class); + var mod = StableFieldUpdater.replaceLongZero(underlying, zeroReplacement); + assertEquals(1L, (long) mod.invoke(1L)); + assertEquals(zeroReplacement, (long) mod.invoke(0L)); + } + + static final class Foo implements HasHashField { + + private static final MethodHandle HASH_UPDATER; + + static { + MethodHandles.Lookup lookup = MethodHandles.lookup(); + try { + VarHandle accessor = lookup.findVarHandle(Foo.class, "hash", int.class); + MethodHandle underlying = lookup.findStatic(Foo.class, "hashCodeFor", MethodType.methodType(int.class, Foo.class)); + HASH_UPDATER = StableFieldUpdater.atMostOnce(accessor, underlying); + } catch (ReflectiveOperationException e) { + throw new InternalError(e); + } + } + + private final String string; + + int hash; + long dummy; + + public Foo(String string) { + this.string = string; + } + + @Override + public int hashCode() { + try { + return (int) HASH_UPDATER.invokeExact(this); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + @Override + public long hash() { + return hash; + } + + private static int hashCodeFor(Foo foo) { + return foo.string.hashCode(); + } + } + + static final class LongFoo implements HasHashField { + + private static final MethodHandle HASH_UPDATER; + + static { + MethodHandles.Lookup lookup = MethodHandles.lookup(); + try { + VarHandle accessor = lookup.findVarHandle(LongFoo.class, "hash", long.class); + MethodHandle underlying = lookup.findStatic(LongFoo.class, "hashCodeFor", MethodType.methodType(long.class, LongFoo.class)); + HASH_UPDATER = StableFieldUpdater.atMostOnce(accessor, underlying); + } catch (ReflectiveOperationException e) { + throw new InternalError(e); + } + } + + private final String string; + + long hash; + long dummy; + + public LongFoo(String string) { + this.string = string; + } + + @Override + public int hashCode() { + try { + return (int) (long) HASH_UPDATER.invokeExact(this); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + @Override + public long hash() { + return hash; + } + + private static long hashCodeFor(LongFoo foo) { + return foo.string.hashCode(); + } + + } + + + + static final class InheritingFoo extends AbstractFoo implements HasHashField { + + private static final MethodHandle HASH_UPDATER; + + static { + MethodHandles.Lookup lookup = MethodHandles.lookup(); + try { + VarHandle accessor = lookup.findVarHandle(InheritingFoo.class, "hash", int.class); + MethodHandle underlying = lookup.findStatic(InheritingFoo.class, "hashCodeFor", MethodType.methodType(int.class, InheritingFoo.class)); + HASH_UPDATER = StableFieldUpdater.atMostOnce(accessor, underlying); + } catch (ReflectiveOperationException e) { + throw new InternalError(e); + } + } + + public InheritingFoo(String string) { + super(string); + } + + @Override + public int hashCode() { + try { + return (int) HASH_UPDATER.invokeExact(this); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + private static int hashCodeFor(InheritingFoo foo) { + return foo.string.hashCode(); + } + } + + static abstract class AbstractFoo implements HasHashField { + final String string; + int hash; + + public AbstractFoo(String string) { + this.string = string; + } + + @Override + public long hash() { + return hash; + } + } + + interface HasHashField { + long hash(); + } + + // Illegal underlying function for int and long + private static double doubleFrom(Foo foo) { + return 1; + } + + // Apparently, `hashCode()` is invoked if we create a stream of just `HasHashField` + // instances so we provide the associated constructors instead. + static Stream> fooConstructors() { + return Stream.of( + Foo::new, + LongFoo::new, + InheritingFoo::new + ); + } + +} diff --git a/test/micro/org/openjdk/bench/java/lang/stable/StableFieldUpdaterBenchmark.java b/test/micro/org/openjdk/bench/java/lang/stable/StableFieldUpdaterBenchmark.java new file mode 100644 index 0000000000000..b05684a89d5c0 --- /dev/null +++ b/test/micro/org/openjdk/bench/java/lang/stable/StableFieldUpdaterBenchmark.java @@ -0,0 +1,154 @@ +/* + * 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. + * + * 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 org.openjdk.bench.java.lang.stable; + +import jdk.internal.lang.stable.StableFieldUpdater; +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.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.invoke.VarHandle; +import java.net.URI; +import java.sql.Ref; +import java.util.concurrent.TimeUnit; +import java.util.function.ToIntFunction; + +/** + * Benchmark measuring StableValue performance + */ +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Benchmark) // Share the same state instance (for contention) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 2) +@Fork(value = 2, jvmArgs = {"--enable-preview", "--add-exports", "java.base/jdk.internal.lang.stable=ALL-UNNAMED"}) +@Threads(Threads.MAX) // Benchmark under contention +public class StableFieldUpdaterBenchmark { + + private static final String STRING = "https://some.site.com"; + + private static final Base BASE = new Base(STRING); + private static final Updater UPDATER = new Updater(STRING); + private static final URI U_R_I = URI.create(STRING); + + private final Base base = new Base(STRING); + private final Updater updater = new Updater(STRING); + private static final URI uri = URI.create(STRING); + + @Benchmark + public int baseStatic() { + return BASE.hashCode(); + } + + @Benchmark + public int base() { + return base.hashCode(); + } + + @Benchmark + public int updaterStatic() { + return UPDATER.hashCode(); + } + + @Benchmark + public int updater() { + return updater.hashCode(); + } + + @Benchmark + public int uriStatic() { + return U_R_I.hashCode(); + } + + @Benchmark + public int uri() { + return uri.hashCode(); + } + + static final class Base extends Abstract { + Base(String string) { super(string); } + + @Override + public int hashCode() { + int h = hashCode; + if (h == 0) { + hashCode = h = string.hashCode(); + } + return h; + } + } + + static final class Updater extends Abstract { + + private static final MethodHandle HASH_UPDATER; + + static { + MethodHandles.Lookup lookup = MethodHandles.lookup(); + try { + var varHandle = lookup.findVarHandle(Updater.class, "hashCode", int.class); + var accessor = lookup.findVirtual(Updater.class, "hashCode0", MethodType.methodType(int.class)); + HASH_UPDATER = StableFieldUpdater.atMostOnce(varHandle, accessor); + } catch (ReflectiveOperationException e) { + throw new InternalError(e); + } + } + + Updater(String string) { super(string); } + + @Override + public int hashCode() { + try { + return (int) HASH_UPDATER.invokeExact(this); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + private int hashCode0() { + return string.hashCode(); + } + + } + + static class Abstract { + + final String string; + int hashCode; + + Abstract(String string) { + this.string = string; + } + } + +}