5

I've been running some microbenchmarks, and have come across an odd issue. I'm using java version "1.8.0_131" with the default compiler options.

Given a definition

public class JavaState {
    public String field = "hello";
    public final String finalField = "hello";
}

accessing field directly (state.field) generates

ALOAD 1
GETFIELD JavaState.field : Ljava/lang/String;

But accessing finalField directly (state.finalField) generates

ALOAD 1
INVOKEVIRTUAL java/lang/Object.getClass ()Ljava/lang/Class;
POP
LDC "hello"

Why bytecode calls Object->getClass() at a direct field access explains that the call to getClass is just to check that state is not null, but then the compiler has inlined the value of the field.

I might reasonably expect that substituting later versions of JavaState with different field values would result in other code seeing the change without recompilation, but this inlining prevents that. And my benchmarks show that if it is done in the name of performance it isn't working; at least on my benchmark Raspberry Pi, accessing finalField is 5-10% slower than accessing field.

What is the rationale for inlining the value of the final field?

Jeffrey Bosboom
  • 13,313
  • 16
  • 79
  • 92
Duncan McGregor
  • 17,665
  • 12
  • 64
  • 118
  • It's clear why simply replacing with a constant load doesn't work, and something is needed to verify that the object reference is not null. But it's still an interesting question why it would be inlined at all - why replace a field access with a method call plus a constant load? Why does the compiler not just leave the field access there? It isn't much of an optimization this way. – Erwin Bolwidt Oct 29 '17 at 00:13
  • And the drawback is that changes in the JavaState class don't get propagated to dependent classes. With HotSpot being an age-old technology, any such optimization could be done at the JVM level without breaking code modularity. Time to remove this "optimization" from the source compiler? – Erwin Bolwidt Oct 29 '17 at 00:19
  • I've updated the body with the getClass information - I think the question `Why does the Java compiler inline access to non-static final fields?` still stands. – Duncan McGregor Oct 29 '17 at 00:23
  • 1
    I'll note that you didn't *show the code* for "accessing `field` directly". Where does it live? – chrylis -cautiouslyoptimistic- Oct 29 '17 at 00:44
  • My guess is that on JIT the invokeVirtual on getClass will be dropped on repetitions. Raspberry Pi might suffer some inefficiencies. – Joop Eggen Oct 29 '17 at 01:01
  • Huh, I'm surprised that `javac` inlines the non-static field - my impression was that only `static final` fields got that treatment (and in particular `javac` is required to inline such values at least when they are primitive). It means that changing a `final` field may be a breaking change as you point out. The reason to inline it is obviously "performance" - but it didn't pan out in this case, perhaps due to the null check. You should share your benchmark though, probably a lot depends on whether the JIT can prove the target non-null and whether the check can be hoisted if not. – BeeOnRope Oct 29 '17 at 01:17
  • I discovered the issue as part of an investigation into Kotlin performance - you can see the code here http://oneeyedmen.com/cost-of-kotlin-preliminary-results-part-5-properties.html – Duncan McGregor Oct 29 '17 at 08:22
  • Be prepared for `object.getClass()` being replaced with `Objects.requireNonNull(object)` with newer compilers, as discussed in [this answer](https://stackoverflow.com/a/43116722/2711488). The inlining of compile-time constants is mandatory for *all* variables, but I could ask the other way round: “What is the rationale for declaring an instance field (that needs memory for each object) that holds a constant?” – Holger Nov 01 '17 at 14:32

1 Answers1

10

This may be mandated by the Java Language Specification, but the details are unclear. From Section 4.12.4 final Variables:

A constant variable is a final variable of primitive type or type String that is initialized with a constant expression (§15.28). Whether a variable is a constant variable or not may have implications with respect to class initialization (§12.4.1), binary compatibility (§13.1, §13.4.9), and definite assignment (§16 (Definite Assignment)).

Note there is no requirement that the variable be static. Then from Section 13.1 The Form of a Binary:

A reference to a field that is a constant variable (§4.12.4) must be resolved at compile time to the value V denoted by the constant variable's initializer.

If such a field is static, [...]

If such a field is non-static, then no reference to the field should be present in the code in a binary file, except in the class containing the field. (It will be a class rather than an interface, since an interface has only static fields.) The class should have code to set the field's value to V during instance creation (§12.5).

I'm not sure where your decompiled code comes from. If it's outside the class, then what you see is mandated by the Specification. If it's inside the class, it's less clear. You could read the third paragraph in the above quote to imply that the only code reference to the field should be in <init> methods initializing the field, but this is not actually stated.

Section 13.4.9 directly addresses your concern about binary compatibility, but seems to limit itself explicitly to static fields (emphasis mine):

If a field is a constant variable (§4.12.4), and moreover is static, then deleting the keyword final or changing its value will not break compatibility with pre-existing binaries by causing them not to run, but they will not see any new value for a usage of the field unless they are recompiled. This result is a side-effect of the decision to support conditional compilation (§14.21). (One might suppose that the new value is not seen if the usage occurs in a constant expression (§15.28) but is seen otherwise. This is not so; pre-existing binaries do not see the new value at all.)

Another reason for requiring inlining of values of static constant variables is because of switch statements. They are the only kind of statement that relies on constant expressions, namely that each case label of a switch statement must be a constant expression whose value is different than every other case label. case labels are often references to static constant variables so it may not be immediately obvious that all the labels have different values. If it is proven that there are no duplicate labels at compile time, then inlining the values into the class file ensures there are no duplicate labels at run time either - a very desirable property.

As non-static constant final fields are not commonly used nor especially useful, it's possible they simply slipped through the cracks when writing the Specification.

Community
  • 1
  • 1
Jeffrey Bosboom
  • 13,313
  • 16
  • 79
  • 92
  • An excellent answer thank you. The code was from outside the class, so, to cut a long story short, “because the JLS says so”. – Duncan McGregor Oct 29 '17 at 08:20
  • The interpretation of the third paragraph that the only code reference to the field should be in `` methods initializing the field, is correct and already implied by the first sentence: *A reference to a field that is a constant variable (§4.12.4) must be resolved at compile time to the value V denoted by the constant variable's initializer*. – Holger Oct 29 '17 at 12:26
  • By the way, this rule is not only independent from the question, whether the variable is a `static` or instance field, it also doesn't matter whether it is a field or a local variable. You can even use local variables as annotation values [or `case` labels](https://stackoverflow.com/a/24160611/2711488), if they are constant variables. – Holger Oct 29 '17 at 12:29
  • One important aspect of this specified behavior is that all constants are immune to initialization order issues, e.g. if a super class constructor invokes an overridden method when the subclass’ field initializers have not executed yet. Or when multi-threading without proper synchronization is involved. See also [this answer](https://stackoverflow.com/a/36608257/2711488)… – Holger Oct 29 '17 at 12:41