JEP draft: Optimize Final Field Loads In Generated Code

Authorvlivanov
OwnerVladimir Ivanov
TypeFeature
ScopeJDK
StatusDraft
Componenthotspot / compiler
Discussion hotspot dash compiler dash dev at openjdk dot java dot net
EffortM
DurationM
Created2015/07/23 13:16
Updated2019/12/05 13:48
Issue8132243

Summary

Enable optimizations in JIT-compilers to constant fold final field loads in generated code.

Goals

The goal is to come up with a set of optimizations in JIT-compilers to constant fold final field loads in generated code. During the course of work opportunities for tightening rules for final field updates at run-time will also be explored.

Motivation

The JVMS and the JMM provide some strong guarantees about final field initialization and visibility.

It's appealing from a performance perspective to exploit them and avoid loading field values which don't change, thus producing more efficient code.

Moreover, optimizations on instance final fields are crucial for performance in some scenarios. For example, JSR 292 (java.lang.invoke) heavily relies on the ability to constant fold loads from final instance fields to get decent invokedynamic performance (there are special cases in the JVM code for now).

Although the HotSpot JVM already optimizes loads from static final fields, it is still very conservative when seeing instance final fields. The reason is that there are scenarios (e.g. deserialization) when the object constructor is skipped and final field values are written after the object is instantiated.

Immutable objects are promoted as safe in concurrent scenarios and are becoming very popular, so many applications should benefit from such optimizations.

Description

The JVMS is already quite restrictive. At the byte-code level, instance final field writes are allowed only in constructors (<init>) and static final field writes only in static initializers (<clinit>).

However, there's a limited set of additional scenarios when final field updates are possible. There are 4 ways to circumvent the limitations and change a final field value at run-time:

The Unsafe API is deliberately left out of scope. It is designed as a simple, well-factored set of building blocks to implement low-level JVM operations and (independently) provide access to some run-time features of the hardware platform. It is a user's responsibility to ensure that performed operations are safe.

Regarding all other cases, JIT-compilers should take all of them into account when optimizing final field loads and either track updates or be conservative and avoid optimizations.

There are 3 approaches being considered:

  1. tighten run-time rules for final field updates: forbid all stores to final fields once the object is fully constructed;

  2. silently nullify (ignore and discard) illegal stores to final fields;

  3. track all final field updates in the JVM and adapt accordingly.

The first approach, tightened rules for illegal final field updates, requires the JVM to throw an exception when a store to final field is performed on a properly constructed object (fail-fast approach). It aligns run-time behavior with the JVMS.

The normal new/<init> byte-code sequence guarantees that the object is properly constructed once constructor has completed.

It liberates the JVM from the responsibility to track all final field updates and throwing away generated code when a field which was optimized earlier changes its value.

However, there are valid use-cases when JVMS restrictions should be relaxed (e.g., deserialization). The common scenario is separate object construction and publication. In such case, the new/<init> sequence doesn't work anymore and non-standard ways to instantiate objects are used. There are 3 ways to create an instance without running a constructor on it:

  1. Unsafe.allocateInstance(Class<?>)

  2. ReflectionFactory.newConstructorForSerialization(Class<?>, Constructor<?>) (used by deserialization)

  3. AllocObject(JNIEnv*, jclazz) in JNI

These functions should produce "slushy" objects - objects which can freely change after they are instantiated. The JVM should allow final field updates for such objects and be conservative when optimizing for them.

The "slushiness" property can be recorded as a flag in the object header.

Since it is the user's responsibility to either invoke a constructor or manually initialize the object, an additional operation ("publish"/"freeze") is needed to signal that construction is over and clear the "slushy" flag. It lets the JVM know that the object construction is finished (no more final field updates are planned), so the JVM can harden checks and optimize operations on final fields from then on.

JIT-compilers consult that flag to gate final-folding. Reflection, JNI, and MethodHandles check the flag when attempting to write to a final field and throw a error if it is not set.

The second approach, silently nullifying stores to final fields in properly constructed objects, is legal in some cases according to the JMM. Nullification is indistinguishable from the store occurring but never being observed by a future read. This is possible if either the store is delayed indefinitely, or if all threads (and compiled methods) have previously performed a caching read of the original final value. Additional investigation should be conducted to ensure that the JMM allows some sort of OOTA caching read of the original final value, since the threads aren't obliged to physically do such a caching read first.

Finally for the third approach, if there are no adjustments to run-time behavior, the JVM has to track all final field updates and adapt accordingly by invalidating all affected generated code.

JNI, java.lang.invoke and the Reflection API should be instrumented with additional checks to notify the JVM when an application attempts to write to a final field. The JVM should track all the dependencies in generated code on final field values.

Risks and Assumptions

There are compatibility risks due to hardened checks in the Reflection API. If an application uses the Reflection API to write final fields, it will get run-time errors when attempting to perform such operations.

External users of sun.misc.Unsafe are affected if they change final fields in a properly constructed object. Such updates aren't guaranteed to be visible, i.e. just as today if static final fields are updated.

There's a risk that a user forgets to perform the "publish" operation and the object stays in "slushy" state forever.

That can be mitigated by providing JVM and library diagnostic functionality to detect runaway slushy objects:

For a JVM-only optimization, experiments showed a considerable increase in recorded dependencies for generated code during run-time. It stresses dependency tracking machinery in the JVM, both in recording (more space needed) and checking (more work to enumerate affected generated code).

The impact should be measured and additional optimizations considered (e.g. more efficient lookup of per-object dependencies, per-class vs per-object dependency tracking) to reduce both the number of dependencies and dependency tracking overhead.

Dependencies

None.