Skip to content

Conversation

minborg
Copy link
Contributor

@minborg minborg commented Jun 10, 2024

Stable Values (Preview)

Summary

This PR proposes to introduce an internal Stable Values & Collections API, which provides immutable value holders where elements are initialized at most once. Stable Values offer the performance and safety benefits of final fields while offering greater flexibility as to the timing of initialization.

Goals

  • Provide an easy and intuitive API to describe value holders that can change at most once.
  • Decouple declaration from initialization without significant footprint or performance penalties.
  • Reduce the amount of static initializer and/or field initialization code.
  • Uphold integrity and consistency, even in a multi-threaded environment.

For more details, see the draft JEP: https://openjdk.org/jeps/8312611

Performance

Performance compared to instance variables using two AtomicReference and two protected by double-checked locking under concurrent access by all threads:

Benchmark                          Mode  Cnt  Score   Error  Units
StableValueBenchmark.atomic        avgt   10  1.338 ? 0.040  ns/op
StableValueBenchmark.dcl           avgt   10  1.452 ? 0.099  ns/op
StableValueBenchmark.stable        avgt   10  1.175 ? 0.046  ns/op
StableValueBenchmark.stableNull    avgt   10  0.967 ? 0.004  ns/op

Performance compared to static variables protected by AtomicReference, class-holder idiom holder, and double-checked locking (all threads):

Benchmark                          Mode  Cnt  Score   Error  Units
StableValueBenchmark.staticAtomic  avgt   10  1.262 ? 0.220  ns/op
StableValueBenchmark.staticDcl     avgt   10  0.358 ? 0.014  ns/op
StableValueBenchmark.staticStable  avgt   10  0.352 ? 0.020  ns/op  <-- Constant folded

All figures above are from local tests on a Mac M1 laptop and should only be constructed as indicative figures.

Implementation details

There are some noteworthy implementation details in this PR:

  • A field is trusted if it is declared as a final StableValue. Previously, the determination of trustworthiness was connected to the class in which it was declared (e.g. is it a record or a hidden class). In order to grant such trust, there are extra restrictions imposed on reflection and sun.misc.Unsafe usage for such declared StableValue fields. This is similar to how record classes are handled.
  • The current implementation uses memory barriers for the holder value which will guarantee visibility across threads. It is believed performance can be further improved via https://bugs.openjdk.org/browse/JDK-8333791
  • This PR is a scaled-down version of the original PR 8330465: Stable Values and Collections (Internal) #18794

Progress

  • Change must be properly reviewed (1 review required, with at least 1 Reviewer)
  • Change must not contain extraneous whitespace
  • Commit message must refer to an issue

Issue

  • JDK-8330465: Stable Values (Preview) (Enhancement - P3)

Reviewing

Using git

Checkout this PR locally:
$ git fetch https://git.openjdk.org/jdk.git pull/19625/head:pull/19625
$ git checkout pull/19625

Update a local copy of the PR:
$ git checkout pull/19625
$ git pull https://git.openjdk.org/jdk.git pull/19625/head

Using Skara CLI tools

Checkout this PR locally:
$ git pr checkout 19625

View PR using the GUI difftool:
$ git pr show -t 19625

Using diff file

Download this PR as a diff file:
https://git.openjdk.org/jdk/pull/19625.diff

@bridgekeeper
Copy link

bridgekeeper bot commented Jun 10, 2024

👋 Welcome back pminborg! A progress list of the required criteria for merging this PR into master will be added to the body of your pull request. There are additional pull request commands available for use with this pull request.

@minborg minborg marked this pull request as draft June 10, 2024 12:04
@openjdk
Copy link

openjdk bot commented Jun 10, 2024

❗ This change is not yet ready to be integrated.
See the Progress checklist in the description for automated requirements.

@openjdk
Copy link

openjdk bot commented Jun 10, 2024

@minborg The following labels will be automatically applied to this pull request:

  • build
  • core-libs
  • hotspot-compiler

When this pull request is ready to be reviewed, an "RFR" email will be sent to the corresponding mailing lists. If you would like to change these labels, use the /label pull request command.

@minborg
Copy link
Contributor Author

minborg commented Aug 8, 2024

After some consideration and benchmarking, we think it is better to use pure volatile semantics for all the load operations rather than first trying normal semantics and then volatile.

For all supported platforms, this does not incur any non-fixable significant performance regressions. It also allows us to promise real happens-before relations between stable value operations.

The implementation becomes much simpler and easier to review.

Comment on lines 802 to 803
// This should never happen
throw new InternalError(
Copy link

Choose a reason for hiding this comment

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

This can happen when the mapper recursively calls the get method on this list.

Suggested change
// This should never happen
throw new InternalError(
throw new ConcurrentModificationException(

or

Suggested change
// This should never happen
throw new InternalError(
throw new IllegalStateException(

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think, if the mapper is recursive, we never reach the stable.trySet() statement. I've added a test to make sure this is the case:

    @Test
    void recursiveCall() {
        AtomicReference<IntFunction<Integer>> ref = new AtomicReference<>();
        var lazy = StableValue.lazyList(SIZE, i -> ref.get().apply(i));
        ref.set(lazy::get);
        assertThrows(StackOverflowError.class, () -> lazy.get(INDEX));
    }

Is there another way we could end up there?

Copy link

@ExE-Boss ExE-Boss Aug 12, 2024

Choose a reason for hiding this comment

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

The following makes it happen:

var mapper = StableValue.<IntFunction<Integer>>newInstance();
var list = StableValue.lazyList(SIZE, i -> mapper.orElseThrow().apply(i));

var recursing = new boolean[SIZE];
mapper.setOrThrow(i -> {
	if (recursing[i]) {
		return i;
	}

	recursing[i] = true;
	return list.get(i);
});

list.get(INDEX);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ahh. But that is cheating.... ;-) Well, I see what you mean now. I will update the exception type.

default void setOrThrow(T value) {
if (!trySet(value)) {
throw new IllegalStateException("Cannot set the holder value to " + value +
" because a holder value is alredy set: " + this);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
" because a holder value is alredy set: " + this);
" because a holder value is already set: " + this);

@sunmisc
Copy link

sunmisc commented Aug 29, 2024

interestingly, when caching, after initialization, you can remove the original supplier = null. But I don't know how the @stable annotation works. But this optimization can be useful for reducing the size of the object (if no one else holds the supplier)


* *brittleness* - the convoluted nature of the code required to write a correct double-checked locking makes it all too easy for developers to make subtle mistakes. A very common one is forgetting to add the `volatile` keyword to the `logger` field.
* *lack of expressiveness* - even when written correctly, the double-checked idiom leaves a lot to be desired. The "at-most-once" mutation guarantee is not explicitly manifest in the code: after all the `logger` field is just a plain mutable field. This leaves important semantics gaps that is impossible to plug. For example, the `logger` field can be accidentally mutated in another method of the `Application` class. In another example, the field might be reflectively mutated using `setAccessible`. Avoiding these pitfalls is ultimately left to developers.
* *lack of optimizations* - as the `logger` field is updated at most once, one might expect the JVM to optimize access to this field accordingly, e.g. by [constant-folding](https://en.wikipedia.org/wiki/Constant_folding) access to an already-initialized `logger` field. Unfortunately, since `logger` is just a plan mutable field, the JVM cannot trust the field to never be updated again. As such, access to at-most-once fields, when realized with double-checked locking is not as efficient as it could be.
Copy link

@chu121su12 chu121su12 Aug 27, 2024

Choose a reason for hiding this comment

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

a planplain mutable field

@bridgekeeper
Copy link

bridgekeeper bot commented Nov 7, 2024

@minborg This pull request has been inactive for more than 8 weeks and will be automatically closed if another 8 weeks passes without any activity. To avoid this, simply add a new comment to the pull request. Feel free to ask for assistance if you need help with progressing this pull request towards integration!

@minborg
Copy link
Contributor Author

minborg commented Nov 14, 2024

Keep alive

@minborg minborg closed this Dec 4, 2024
@gredler
Copy link
Contributor

gredler commented Dec 6, 2024

@minborg What's the current status of this StableValue work? It looks like this PR has been closed, but the code is not merged? Is it likely to be ready for internal JDK use in Java 25, or will internal uses need to wait until after the preview phase? There's some disagreement on the best approach for JDK-8337505 in the desktop module, and StableValue might be a good resolution... if it's ready soon.

@liach
Copy link
Member

liach commented Dec 6, 2024

I think we are looking for 25. There is no point of internal use in java.base, but it might help other modules. For the specific issue you've linked, I think a lazy holder pattern may be sufficient.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging this pull request may close these issues.

7 participants