Skip to content

Commit ee80875

Browse files
committed
[GR-68726] Delay the folding of most @stable fields until the analysis is finished
PullRequest: graal/21863
2 parents cd9de56 + dc44519 commit ee80875

File tree

7 files changed

+236
-19
lines changed

7 files changed

+236
-19
lines changed

compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/core/common/spi/JavaConstantFieldProvider.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,11 @@ private static ResolvedJavaField findField(ResolvedJavaType type, String fieldNa
7070
public <T> T readConstantField(ResolvedJavaField field, ConstantFieldTool<T> tool) {
7171
if (isStableField(field, tool)) {
7272
JavaConstant value = tool.readValue();
73-
if (value != null && isStableFieldValueConstant(field, value, tool)) {
74-
return foldStableArray(value, field, tool);
73+
if (value != null) {
74+
onStableFieldRead(field, value, tool);
75+
if (isStableFieldValueConstant(field, value, tool)) {
76+
return foldStableArray(value, field, tool);
77+
}
7578
}
7679
}
7780
if (isFinalField(field, tool)) {
@@ -83,6 +86,17 @@ public <T> T readConstantField(ResolvedJavaField field, ConstantFieldTool<T> too
8386
return null;
8487
}
8588

89+
/**
90+
* Hook for subclasses to inspect the {@code value} read from the given {@code field}. The value
91+
* can be the default for the given kind (i.e. the field will not actually be folded). The
92+
* {@code value} will never be {@code null}, but it may be a {@code JavaConstant} representing
93+
* null, which will happen when an object field with the default value is read.
94+
*/
95+
@SuppressWarnings("unused")
96+
protected void onStableFieldRead(ResolvedJavaField field, JavaConstant value, ConstantFieldTool<?> tool) {
97+
98+
}
99+
86100
protected <T> T foldStableArray(JavaConstant value, ResolvedJavaField field, ConstantFieldTool<T> tool) {
87101
return tool.foldStableArray(value, getArrayDimension(field.getType()), isDefaultStableField(field, tool));
88102
}

substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/FeatureImpl.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2013, 2023, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2013, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -101,6 +101,7 @@
101101
import jdk.graal.compiler.debug.Assertions;
102102
import jdk.graal.compiler.debug.DebugContext;
103103
import jdk.graal.compiler.phases.util.Providers;
104+
import jdk.internal.vm.annotation.Stable;
104105
import jdk.vm.ci.meta.MetaAccessProvider;
105106

106107
@SuppressWarnings("deprecation")
@@ -563,6 +564,26 @@ public void registerOpaqueMethodReturn(Method method) {
563564
VMError.guarantee(aMethod.getAllMultiMethods().size() == 1, "Opaque method return called for method with >1 multimethods: %s ", method);
564565
aMethod.setOpaqueReturn();
565566
}
567+
568+
/**
569+
* Calling this method allows a given {@link Stable} {@code field} to be folded before
570+
* analysis, which may improve analysis precision and allow more classes to be initialized
571+
* via simulation. Users of this method are <b>required to somehow initialize the field
572+
* before analysis</b>, ideally next to the call to {@code allowStableFieldFolding} or at
573+
* least write a comment there to explain how the {@code field} is initialized. Trying to
574+
* fold such a {@link Stable} field which still has a default value by the time the analysis
575+
* is started results in a <b>build failure</b>, because if the initialization occurs later,
576+
* the folding might result in a non-deterministic behavior, as the field will get folded
577+
* before/during analysis only in some builds when the initialization happened fast enough,
578+
* resulting in unstable number of reachable methods and unstable decisions of the
579+
* simulation of class initializers.
580+
*
581+
* @see SVMHost#allowStableFieldFoldingBeforeAnalysis
582+
*/
583+
public void allowStableFieldFoldingBeforeAnalysis(Field field) {
584+
VMError.guarantee(field.isAnnotationPresent(Stable.class), "This method should only be called for @Stable fields: %s", field);
585+
getHostVM().allowStableFieldFoldingBeforeAnalysis(getMetaAccess().lookupJavaField(field));
586+
}
566587
}
567588

568589
public static class DuringAnalysisAccessImpl extends BeforeAnalysisAccessImpl implements Feature.DuringAnalysisAccess {

substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/NativeImageGenerator.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,7 @@ protected boolean runPointsToAnalysis(String imageName, OptionValues options, De
814814
BeforeAnalysisAccessImpl config = new BeforeAnalysisAccessImpl(featureHandler, loader, bb, nativeLibraries, debug);
815815
ServiceCatalogSupport.singleton().enableServiceCatalogMapTransformer(config);
816816
featureHandler.forEachFeature(feature -> feature.beforeAnalysis(config));
817+
bb.getHostVM().checkWellKnownStableFieldsBeforeAnalysis(bb);
817818
ServiceCatalogSupport.singleton().seal();
818819
bb.getHostVM().getClassInitializationSupport().sealConfiguration();
819820
if (ImageLayerBuildingSupport.buildingImageLayer()) {

substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/SVMHost.java

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@
167167
import jdk.internal.reflect.Reflection;
168168
import jdk.internal.vm.annotation.DontInline;
169169
import jdk.internal.vm.annotation.ForceInline;
170+
import jdk.internal.vm.annotation.Stable;
170171
import jdk.vm.ci.meta.DeoptimizationReason;
171172
import jdk.vm.ci.meta.JavaConstant;
172173
import jdk.vm.ci.meta.MetaAccessProvider;
@@ -206,6 +207,15 @@ public enum UsageKind {
206207
private final ConcurrentMap<AnalysisMethod, Boolean> analysisTrivialMethods = new ConcurrentHashMap<>();
207208

208209
private final Set<AnalysisField> finalFieldsInitializedOutsideOfConstructor = ConcurrentHashMap.newKeySet();
210+
/**
211+
* We do not allow the folding of arbitrary {@link Stable} fields if the analysis has not
212+
* finished yet as it leads to non-deterministic results, because the initialization code often
213+
* runs in parallel with the analysis, creating race conditions. However, we need to allow the
214+
* folding of {@link Stable} fields in some cases to improve analysis precision. If we know that
215+
* such fields are initialized before parsing any method that reads their values, the analysis
216+
* results should still be deterministic.
217+
*/
218+
private final Set<AnalysisField> stableFieldsToFoldBeforeAnalysis = ConcurrentHashMap.newKeySet();
209219
private final MultiMethodAnalysisPolicy multiMethodAnalysisPolicy;
210220
private final SVMParsingSupport parsingSupport;
211221
private final InlineBeforeAnalysisPolicy inlineBeforeAnalysisPolicy;
@@ -1298,11 +1308,65 @@ public void recordFieldStore(ResolvedJavaField field, ResolvedJavaMethod method)
12981308
}
12991309
}
13001310

1311+
/**
1312+
* Check the set of well-known {@link Stable} fields and if any of them is already initialized
1313+
* (has a non-default value), allow its constant folding. This method should be called <b>before
1314+
* analysis</b> but after all {@link Feature#beforeAnalysis} callbacks finished to give features
1315+
* a chance to initialize the {@link Stable} fields.
1316+
*
1317+
* @see #stableFieldsToFoldBeforeAnalysis
1318+
*
1319+
* @implNote The "set" is currently only a single field {@code Unsafe.memoryAccessWarned}, but
1320+
* we may extend that in the future.
1321+
*/
1322+
public void checkWellKnownStableFieldsBeforeAnalysis(BigBang bb) {
1323+
assert !BuildPhaseProvider.isAnalysisStarted() : "This method should be called before the analysis is started.";
1324+
/*
1325+
* Folding of Unsafe.memoryAccessWarned is important to remove the code that prints the
1326+
* warning when unsafe is used for the first time (see Unsafe.beforeMemoryAccess), which
1327+
* would otherwise be marked as reachable. Unsafe.memoryAccessWarned is not initialized
1328+
* early enough in every build, but if it is already initialized by this point, it is
1329+
* beneficial to fold it.
1330+
*/
1331+
var field = ReflectionUtil.lookupField(loader.findClassOrFail("sun.misc.Unsafe"), "memoryAccessWarned");
1332+
try {
1333+
var memoryAccessWarned = (boolean) field.get(null);
1334+
if (memoryAccessWarned) {
1335+
allowStableFieldFoldingBeforeAnalysis(bb.getMetaAccess().lookupJavaField(field));
1336+
}
1337+
} catch (IllegalAccessException e) {
1338+
throw VMError.shouldNotReachHere(e);
1339+
}
1340+
}
1341+
1342+
/**
1343+
* @see #stableFieldsToFoldBeforeAnalysis
1344+
*/
13011345
public boolean allowConstantFolding(ResolvedJavaField field) {
13021346
AnalysisField aField = field instanceof HostedField ? ((HostedField) field).getWrapped() : (AnalysisField) field;
1347+
if (!BuildPhaseProvider.isAnalysisFinished() && !aField.isFinal() && aField.isAnnotationPresent(Stable.class)) {
1348+
return stableFieldsToFoldBeforeAnalysis.contains(aField);
1349+
}
13031350
return !finalFieldsInitializedOutsideOfConstructor.contains(aField);
13041351
}
13051352

1353+
/**
1354+
* Allows the given {@link Stable} field to be folded before analysis. Use with caution and only
1355+
* when you know that the given field is initialized before any method that reads its value is
1356+
* parsed, because trying to fold such a {@link Stable} field which still has a default value by
1357+
* the time the analysis is started results in a <b>build failure</b>, because if the
1358+
* initialization occurs later, the folding might result in <i>non-deterministic behavior</i>,
1359+
* as the field will get folded before/during analysis only in some builds when the
1360+
* initialization happened fast enough, resulting in unstable number of reachable methods and
1361+
* unstable decisions of the simulation of class initializers.
1362+
*
1363+
* @see #stableFieldsToFoldBeforeAnalysis
1364+
* @see SimulateClassInitializerSupport
1365+
*/
1366+
public void allowStableFieldFoldingBeforeAnalysis(AnalysisField field) {
1367+
stableFieldsToFoldBeforeAnalysis.add(field);
1368+
}
1369+
13061370
@Override
13071371
public Object parseGraph(BigBang bb, DebugContext debug, AnalysisMethod method) {
13081372
if (parsingSupport != null) {

substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ameta/AnalysisConstantFieldProvider.java

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2016, 2017, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2016, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -24,14 +24,24 @@
2424
*/
2525
package com.oracle.svm.hosted.ameta;
2626

27+
import java.lang.invoke.VarHandle;
28+
2729
import org.graalvm.nativeimage.Platform;
2830
import org.graalvm.nativeimage.Platforms;
2931

32+
import com.oracle.graal.pointsto.heap.ImageHeapConstant;
3033
import com.oracle.graal.pointsto.meta.AnalysisField;
3134
import com.oracle.svm.core.BuildPhaseProvider;
35+
import com.oracle.svm.core.util.VMError;
3236
import com.oracle.svm.hosted.SVMHost;
37+
import com.oracle.svm.hosted.classinitialization.SimulateClassInitializerSupport;
38+
import com.oracle.svm.hosted.jdk.JDKInitializationFeature;
39+
import com.oracle.svm.hosted.jdk.VarHandleFeature;
3340
import com.oracle.svm.hosted.meta.SharedConstantFieldProvider;
3441

42+
import jdk.graal.compiler.virtual.phases.ea.PartialEscapePhase;
43+
import jdk.internal.vm.annotation.Stable;
44+
import jdk.vm.ci.meta.JavaConstant;
3545
import jdk.vm.ci.meta.MetaAccessProvider;
3646
import jdk.vm.ci.meta.ResolvedJavaField;
3747

@@ -56,6 +66,48 @@ public <T> T readConstantField(ResolvedJavaField f, ConstantFieldTool<T> analysi
5666
return foldedValue;
5767
}
5868

69+
/**
70+
* Before analysis, we fold only explicitly registered {@link Stable} fields (see
71+
* {@link SVMHost#allowStableFieldFoldingBeforeAnalysis}) that should always be initialized by
72+
* then (contain a non-default value).
73+
* <p>
74+
* There are two edge cases:
75+
* <p>
76+
* 1) {@code Module#enableNativeAccess}: We optimistically try to fold this field even though we
77+
* cannot fold it for every Module, because it significantly reduces the size of smaller images
78+
* like helloworld, as discussed in {@link JDKInitializationFeature#beforeAnalysis}. Folding
79+
* this particular field only on some accesses does not result in non-determinism, because in
80+
* those cases where it is set at build time, it happens before analysis.
81+
* <p>
82+
* 2) We allow folding default values from virtualized objects. If an object is virtualized by
83+
* {@link PartialEscapePhase}, its fields cannot be reassigned, so it is safe to fold its
84+
* fields. This sometimes happens with {@link VarHandle}s. Fields whose
85+
* {@link ImageHeapConstant#isBackedByHostedObject} returns {@code false}, are the result of
86+
* class initializer simulation.
87+
*
88+
* @see SVMHost#allowConstantFolding(ResolvedJavaField)
89+
* @see JDKInitializationFeature#beforeAnalysis
90+
* @see PartialEscapePhase
91+
* @see VarHandleFeature
92+
* @see SimulateClassInitializerSupport
93+
*/
94+
@Override
95+
protected void onStableFieldRead(ResolvedJavaField field, JavaConstant value, ConstantFieldTool<?> tool) {
96+
if (value.isDefaultForKind() && !BuildPhaseProvider.isAnalysisFinished() && !field.isFinal() && field.isAnnotationPresent(Stable.class)) {
97+
if (field.getName().equals("enableNativeAccess") && field.getDeclaringClass().getName().equals("Ljava/lang/Module;")) {
98+
/* Edge case 1) */
99+
return;
100+
}
101+
if (!field.isStatic() && tool.getReceiver() instanceof ImageHeapConstant heapConstant && !heapConstant.isBackedByHostedObject()) {
102+
/* Edge case 2) */
103+
return;
104+
}
105+
throw VMError.shouldNotReachHere("Attempting to fold an uninitialized @Stable field before analysis: '%s'. " +
106+
"This suggests that the code that is supposed to initialize this field was not executed yet. " +
107+
"Please ensure that the initialization code for the field runs before any method that accesses it is parsed.", field.format("%H.%n"));
108+
}
109+
}
110+
59111
@Override
60112
protected AnalysisField asAnalysisField(ResolvedJavaField field) {
61113
return (AnalysisField) field;

substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/JDKInitializationFeature.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
package com.oracle.svm.hosted.jdk;
2626

2727
import java.lang.reflect.Field;
28+
import java.security.CodeSource;
2829

2930
import org.graalvm.nativeimage.ImageSingletons;
3031
import org.graalvm.nativeimage.Platform;
@@ -35,6 +36,8 @@
3536
import com.oracle.svm.core.ParsingReason;
3637
import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature;
3738
import com.oracle.svm.core.feature.InternalFeature;
39+
import com.oracle.svm.core.jdk.ProtectionDomainSupport;
40+
import com.oracle.svm.hosted.FeatureImpl;
3841
import com.oracle.svm.hosted.FeatureImpl.AfterRegistrationAccessImpl;
3942
import com.oracle.svm.hosted.ImageClassLoader;
4043
import com.oracle.svm.util.ReflectionUtil;
@@ -303,6 +306,31 @@ public void registerInvocationPlugins(Providers providers, GraphBuilderConfigura
303306
r.register(new ModuleEnableNativeAccessPlugin());
304307
}
305308

309+
/**
310+
* For modules in the image heap, {@code Module#enableNativeAccess} is fixed at build time. We
311+
* want to fold native access checks for build time modules to avoid run-time overheads as well
312+
* as pulling in the error handling code unnecessarily. For example: not folding this would
313+
* increase the size of hello worlds by 2x.
314+
* <p>
315+
* Why does this field impact the number of reachable methods so much?
316+
* {@link ProtectionDomainSupport} and related {@code ProtectionDomainFeature} have a
317+
* reachability handler for {@link CodeSource#getLocation}, which is not reachable by default in
318+
* helloworld, but becomes reachable if {@code Module#enableNativeAccess} is not folded. The
319+
* difference comes from {@code PosixNativeLibrarySupport#initializeBuiltinLibraries}, which
320+
* transitively calls into {@code Module#ensureNativeAccess} and if {@code enableNativeAccess}
321+
* is folded, the call to {@link CodeSource#getLocation} inside
322+
* {@code Module#ensureNativeAccess} can be removed as dead code during inlining before
323+
* analysis.
324+
* <p>
325+
* The modules for which this folding matters are {@code java.base} and
326+
* {@code org.graalvm.nativeimage.builder}), both of which have {@code enableNativeAccess}
327+
* already set by the time the builder starts running.
328+
*/
329+
@Override
330+
public void beforeAnalysis(BeforeAnalysisAccess access) {
331+
((FeatureImpl.BeforeAnalysisAccessImpl) access).allowStableFieldFoldingBeforeAnalysis(ModuleEnableNativeAccessPlugin.ENABLE_NATIVE_ACCESS_FIELD);
332+
}
333+
306334
/**
307335
* Inlines calls to {@code Module$EnableNativeAccess#isNativeAccessEnabled()} if and only if
308336
* {@code Module#enableNativeAccess} is true. This is ok because the field is {@code @Stable},

0 commit comments

Comments
 (0)