Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inline methods that fold to a constant before the static analysis #2860

Conversation

marjanasolajic
Copy link
Contributor

@marjanasolajic marjanasolajic commented Sep 19, 2020

NativeImageInlineDuringParsingPlugin is a plugin that analyses the graph for the resolved Java method and specifies
what should be inlined during graph parsing before the static analysis. This plugin inlines methods that fold to a constant before the static analysis.

The native-image option for this plugin: -H:+InlineBeforeAnalysis.
The native-image option for inlining depth that is used in this plugin: -H:InlineBeforeAnalysisMaxDepth=value.
Default value for inlining depth is 9.

The performance impact of this plugin on analysis time and image size for benchmark suite renaissance is presented in the next documents.
statistics_table
statistics

@graalvmbot
Copy link
Collaborator

Hello Marjana Solajic, thanks for contributing a PR to our project!

We use the Oracle Contributor Agreement to make the copyright of contributions clear. We don't have a record of you having signed this yet, based on your email address marjana -(dot)- s1010 -(at)- gmail -(dot)- com. You can sign it at that link.

If you think you've already signed it, please comment below and we'll check.

@marjanasolajic marjanasolajic force-pushed the plugin-inline-during-parsing branch 2 times, most recently from a42457f to 6a6bfbc Compare September 19, 2020 18:51
@marjanasolajic marjanasolajic changed the title Inline During Parsing Plugin Inline method that fold to a constant before analysis Sep 21, 2020
@@ -88,5 +96,10 @@ protected boolean tryInvocationPlugin(InvokeKind invokeKind, ValueNode[] args, R
public boolean canDeferPlugin(GeneratedInvocationPlugin plugin) {
return plugin.getSource().equals(Fold.class) || plugin.getSource().equals(Node.NodeIntrinsic.class);
}

@Override
protected Invoke createNonInlinedInvoke(ExceptionEdgeAction exceptionEdge, int invokeBci, CallTargetNode callTarget, JavaKind resultType) {
Copy link
Member

Choose a reason for hiding this comment

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

Seems like an unnecessary override. Same in HostedGraphBuilderPhase.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed


public static class Options {
@Option(help = "Inline methods which folds to constant during parsing before the static analysis.")//
public static final HostedOptionKey<Boolean> InlineBeforeAnalysis = new HostedOptionKey<>(false);
Copy link
Member

Choose a reason for hiding this comment

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

Why is it not enabled by default?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

changed

* After analysis we read information from this map to find inline decision we made using
* {@link #getResult}.
*/
private static final ConcurrentHashMap<AnalysisMethod, ConcurrentHashMap<CallSite, InvocationResult>> dataInline = new ConcurrentHashMap<>();
Copy link
Member

Choose a reason for hiding this comment

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

We cannot use any static fields. Many image builds can run in the same VM, and no such state must survive a build. The standard solution is to put such information in an object registered in ImageSingletons

Copy link
Contributor Author

Choose a reason for hiding this comment

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

changed


private static ConcurrentMap<CallSite, InvocationResult> callSitemap(ResolvedJavaMethod method) {
AnalysisMethod key;
if (method instanceof AnalysisMethod) {
Copy link
Member

Choose a reason for hiding this comment

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

There is toAnalysisMethod as a helper method for this

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

public static InvocationResult findResult(String methodName, String className) {
InvocationResult result = null;
for (Map.Entry<AnalysisMethod, ConcurrentHashMap<CallSite, InvocationResult>> pair : dataInline.entrySet()) {
if (pair.getKey().format("%n %H").equals(methodName + " " + className)) {
Copy link
Member

Choose a reason for hiding this comment

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

No, don't use string formatting to check for a nested name. Even in a method only used for testing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed, we can use now information from an object registered in ImageSingletons for tests

Collection<InvocationResult> results = pair.getValue().values();
/* in tests we call method only once */
assert results.size() == 1;
result = results.iterator().next();
Copy link
Member

Choose a reason for hiding this comment

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

What if methodName is not unique?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed

@@ -151,7 +151,8 @@ protected void verifyParameters(MethodCallTargetNode callTarget, StructuredGraph
"org.graalvm.compiler.core.test.VerifyDebugUsageTest$InvalidConcatDumpUsagePhase.run",
"org.graalvm.compiler.core.test.VerifyDebugUsageTest$InvalidDumpUsagePhase.run",
"org.graalvm.compiler.hotspot.SymbolicSnippetEncoder.verifySnippetEncodeDecode",
"org.graalvm.compiler.truffle.compiler.phases.inlining.CallTree.dumpBasic"));
"org.graalvm.compiler.truffle.compiler.phases.inlining.CallTree.dumpBasic",
"com.oracle.svm.hosted.phases.TrivialMethodDetector.analyzeMethod"));
Copy link
Member

Choose a reason for hiding this comment

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

We certainly don't want to dump anything at level 1 when inlining during analysis. Maybe at level 2, could also be level 3.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

/* Nothing to do, it's ok to read a static or instance field. */
} else if (node instanceof FrameState) {
/* Nothing to do */
} else if (node instanceof NewInstanceNode || node instanceof NewArrayNode || node instanceof CallTargetNode || node instanceof StoreFieldNode || node instanceof InvokeNode) {
Copy link
Member

Choose a reason for hiding this comment

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

The PR says "This plugin inlines methods which folds to a constant". So why allow CallTargetNode and StoreFieldNode here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed, for now, only left NewArrayNode because we need this for the #2500

*/
VMError.guarantee(previous == null, "Detected previously intrinsified element");
Object previous = analysisElements.putIfAbsent(new CallSiteDescriptor(method, bci), nonNullElement);
VMError.guarantee(previous == null || previous.equals(nonNullElement), "Newly intrinsified element (" + nonNullElement + ") different than the previous (" + previous + ")");
Copy link
Member

Choose a reason for hiding this comment

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

Using equals here to compare the previous and new element is dangerous. For example, for java.lang.reflect.Method two methods can be equals but still separate objects, which will lead to problems.

You must check for previous == nonNullElement here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

@@ -320,6 +321,9 @@ private static boolean processGetConstructor(GraphBuilderContext b, ResolvedJava
/* We are analyzing the static initializers and should always intrinsify. */
return element;
}
if (((SharedGraphBuilderPhase.SharedBytecodeParser) context).getGraphBuilderConfig().getPlugins().getParameterPlugins().length > 0) {
Copy link
Member

Choose a reason for hiding this comment

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

What is this check doing? But relying on having a parameter plugin registered is most certainly wrong, because we cannot rely on a side effect like this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed

@marjanasolajic marjanasolajic force-pushed the plugin-inline-during-parsing branch 2 times, most recently from 76930fb to 4e4ee4a Compare September 26, 2020 13:41
@marjanasolajic marjanasolajic changed the title Inline method that fold to a constant before analysis Inline method that folds to a constant before analysis Sep 26, 2020
@marjanasolajic marjanasolajic force-pushed the plugin-inline-during-parsing branch 4 times, most recently from d5a8a6c to 7ee4c06 Compare October 2, 2020 20:24
@marjanasolajic marjanasolajic force-pushed the plugin-inline-during-parsing branch 8 times, most recently from b993cad to 71da1a3 Compare October 23, 2020 14:44
Copy link
Member

@cstancu cstancu left a comment

Choose a reason for hiding this comment

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

@marjanasolajic I reviewed your PR. Looks good, most of my comments refer to cosmetic changes. The only real question is the one related to the performance impact of this analysis and the fact that it looks like the analysis is repeated for a call site when that could be avoided.

}

if (callee.getAnnotation(NeverInline.class) != null || callee.getAnnotation(NeverInlineTrivial.class) != null || b.getMethod().getAnnotation(NeverInline.class) != null ||
b.getMethod().getAnnotation(NeverInlineTrivial.class) != null) {
Copy link
Member

Choose a reason for hiding this comment

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

Does a @NeverInline annotation on a caller prevent inlining of any callees?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed for caller, that was a mistake, left only for a callee.

InvocationResult inline;

if (analysis) {
inline = ImageSingletons.lookup(NativeImageInlineDuringParsingPlugin.class).get(b.getMethod(), b.bci(), callee);
Copy link
Member

Choose a reason for hiding this comment

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

The ImageSingletons.lookup(...) line is identical in both branches you can pull it up: InvocationResult inline = ImageSingletons.lookup(...) and remove else branch.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed

@@ -76,6 +76,8 @@ protected void run(StructuredGraph graph) {
private final boolean explicitExceptionEdges;
private final boolean allowIncompleteClassPath;

protected NativeImageInlineDuringParsingPlugin.InvocationResultInline inlineDuringParsingState;
Copy link
Member

Choose a reason for hiding this comment

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

Please document inlineDuringParsingState. It seems like it is used to store inline data for the currently parsed invoke.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, that is its purpose. Added.

static final class CallSite {
final AnalysisMethod caller;
final int bci;
final AnalysisMethod callee;
Copy link
Member

Choose a reason for hiding this comment

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

Is the callee field needed? Doesn't seem to be used anywhere and it is excluded from the equality check.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right, I forgot to include this field in the check. Fixed.

}

@Override
public int hashCode() {
Copy link
Member

Choose a reason for hiding this comment

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

This method can be removed, implementation is identical with super method.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed.

return InvocationResult.ANALYSIS_TOO_COMPLICATED;
}

MethodNodeTrackingAndInline methodState = new MethodNodeTrackingAndInline(new InvocationResultInline(callSite, method));
Copy link
Member

Choose a reason for hiding this comment

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

MethodNodeTrackingAndInline.result is not actually used in the class, it is just stored. Why not just allocate the InvocationResultInline below, in the return statement?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually, we use it to store decisions about children inlining.


CallSite callSite = new CallSite((AnalysisMethod) b.getMethod(), b.bci(), NativeImageInlineDuringParsingPlugin.toAnalysisMethod(callee));
InvocationResult state = analyzeMethod(callSite, (AnalysisMethod) callee, args);
ImageSingletons.lookup(NativeImageInlineDuringParsingPlugin.class).add(b.getMethod(), b.bci(), (AnalysisMethod) callee, state);
Copy link
Member

Choose a reason for hiding this comment

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

If this call site is reached by multiple call chains you run the analysis each time, then check if the results match. Wouldn't it be more efficient to run the analysis just once for each call site? What is the performance impact of this analysis on image build time?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, you are right. Thanks for the advice. The way of method analysis is changed. We now remember the whole bci chain of calling context.

* We collect information during graph construction to filter non-trivial methods and inline
* trivial invokes. The result is used in {@link #analyzeMethod}.
*/
class MethodNodeTrackingAndInline extends NodeEventListener implements InlineInvokePlugin {
Copy link
Member

Choose a reason for hiding this comment

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

I think this can be split into two classes one that extends NodeEventListener and one that implements InlineInvokePlugin, there is no shared state between the two so I think it is cleaner if they are separated, just like you do with the TrivialMethodDetectorParameterPlugin.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done, thanks for the suggestion.

for (InlineInvokePlugin plugin : inlineInvokePlugins) {
plugin.notifyAfterInline(inlineMethod);
if (notifyGraphBuilderContext == null) {
notifyGraphBuilderContext = new PENonAppendGraphBuilderContext(methodScope, invokeData.invoke);
Copy link
Member

Choose a reason for hiding this comment

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

notifyGraphBuilderContext can be initialized outside the loop. If you want to init it only when there are some inline plugins installed you can check the length of inlineInvokePlugins. Seems cleaner that way.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

InvocationResult inline;

if (analysis) {
inline = ImageSingletons.lookup(NativeImageInlineDuringParsingPlugin.class).get(b.getMethod(), b.bci(), callee);
Copy link
Member

Choose a reason for hiding this comment

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

The ImageSingletons.lookup(NativeImageInlineDuringParsingPlugin.class) pattern repets several times. A NativeImageInlineDuringParsingPlugin.singleton() method that returns the value would improve readability.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done, thanks for the suggestion.

@marjanasolajic marjanasolajic force-pushed the plugin-inline-during-parsing branch 3 times, most recently from 78c43cc to 9b79049 Compare October 24, 2020 21:39
@marjanasolajic marjanasolajic force-pushed the plugin-inline-during-parsing branch 2 times, most recently from c244a65 to a3beed2 Compare November 30, 2020 09:08
@marjanasolajic marjanasolajic force-pushed the plugin-inline-during-parsing branch 7 times, most recently from e7aa891 to 47baf31 Compare December 3, 2020 13:00
@vjovanov vjovanov changed the title Inline method that folds to a constant before analysis Inline methods that folds to a constant before analysis Dec 3, 2020
@@ -350,6 +350,10 @@ public JSRData copy() {
this.successors = new ArrayList<>();
}

public boolean bciNotUnique() {
Copy link
Member

Choose a reason for hiding this comment

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

Style: avoid negated methods names, so name the method bciUnique

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

@@ -3958,6 +3973,11 @@ public int bci() {
return stream.currentBCI();
}

@Override
public boolean hasBciDuplication() {
Copy link
Member

Choose a reason for hiding this comment

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

Style: method name here and in BlockMap should match

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

@@ -1134,6 +1139,11 @@ public static void registerGraphBuilderPlugins(FeatureHandler featureHandler, Ru
SubstrateReplacements replacements = (SubstrateReplacements) providers.getReplacements();
plugins.appendInlineInvokePlugin(replacements);

if (nativeImageInlineDuringParsingEnabled()) {
NativeImageInlineDuringParsingPlugin nativeImageInlineDuringParsingPlugin = new NativeImageInlineDuringParsingPlugin(analysis, providers);
Copy link
Member

Choose a reason for hiding this comment

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

Style: not having a local variable here actually makes the line shorter

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

return b.getCallingContext().stream().map(Pair::getLeft).anyMatch(caller -> caller.equals(callee));
}

private static boolean hasInvokeArgument(ValueNode[] args) {
Copy link
Member

Choose a reason for hiding this comment

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

Why does an invoke preclude inlining? That needs at least a good comment for documentation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is actually unnecessary because we will certainly not inline method if there is an invoke node in the graph. I removed this.


private static boolean hasInvokeArgument(ValueNode[] args) {
for (ValueNode arg : args) {
if (arg instanceof InvokeNode) {
Copy link
Member

Choose a reason for hiding this comment

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

InvokeNode is not general enough because there is also InvokeWithExceptionNode. You need to check for the interface Invoke to capture both.

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 removed the whole method.

} catch (Throwable ex) {
debug.dump(DebugContext.VERBOSE_LEVEL, graph, "InlineDuringParsingAnalysis failed with %s", ex);
/*
* Whatever happens during the analysis is non-fatal because we can just not inline that
Copy link
Member

Choose a reason for hiding this comment

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

That is certainly a safe policy for production, but also hides stupid programming errors (like a NullPointerException somewhere in the analysis). So maybe not ignore exceptions when assertions are enabled in the image builder?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

}
} else if (node instanceof StoreFieldNode) {
/*
* We don't inline this method but go further in the analysis to check if any
Copy link
Member

Choose a reason for hiding this comment

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

I don't understand this handling of StoreFieldNode. Why "go further in the analysis" if in the end the result is "not inline"? analyzeGraph just returns false when this point was reached.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are right, it is unnecessary to go further in the analysis in this case. I removed it.

}

CallSite callSite = new CallSite(b.getCallingContext(), NativeImageInlineDuringParsingPlugin.toAnalysisMethod(callee));
InvocationResult inline = analyzeMethod(callSite, (AnalysisMethod) callee, args);
Copy link
Member

Choose a reason for hiding this comment

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

If we call here analyzeMethod, we can create infinite recursion. I don't think we should make a new graph in this case, but rather reuse only the analysis.

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 added fix for this.

}

private static boolean filterStringConcatGeneratedMethods(ResolvedJavaMethod method) {
return method.format("%H.%n").equals("java.lang.String.valueOf") || method.format("%H.%n").equals("java.lang.StringConcatHelper.prepend") ||
Copy link
Member

Choose a reason for hiding this comment

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

This kind of code is OK during testing, however, it is absolutely not OK in production due to style, performance, and correctness issues. This kind of code, makes people feel bad about the whole approach and the reviewer gets serious doubts about the credibility of the whole approach.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, we cannot really merge that. But the proper fix should be really simple: while we are in a graph builder plugin (or any kind of intrinsic context), BytecodeParser.bciUnique needs to return false because any plugin can lead to more than one "replaced invoke" being produced.

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 added fix for this and removed method filterStringConcatGeneratedMethods.

@vjovanov
Copy link
Member

vjovanov commented Dec 4, 2020

I am OK that this makes it into 21.0 as it is useful for experimenting with reflection, however:

  1. The feature must be disabled by default and not announced publicly but only to interested parties.
  2. The infinite recursion issue must be addressed before the feature freeze.
  3. I would rename NativeImageInlineDuringParsingPlugin to ExperimentalNativeImageInlineDuringParsingPlugin and I would note in the comments that this code is for testing purposes only and that it will be replaced with a more elegant version.
  4. The feature must pass all tests when enabled by default.

@vjovanov vjovanov self-assigned this Dec 4, 2020
@vjovanov vjovanov added this to To do in Native Image via automation Dec 4, 2020

public static class Options {
@Option(help = "Inline methods which folds to constant during parsing before the static analysis.")//
public static final HostedOptionKey<Boolean> InlineBeforeAnalysis = new HostedOptionKey<>(true);
Copy link
Member

Choose a reason for hiding this comment

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

If we do not enable the option by default in this PR, then the option help text should start with "Experimental: "

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

@graalvmbot
Copy link
Collaborator

Hello marjanasolajic, thanks for contributing a PR to our project!

We use the Oracle Contributor Agreement to make the copyright of contributions clear. We don't have a record of you having signed this yet, based on your email address 47460123+marjanasolajic -(at)- users -(dot)- noreply -(dot)- github -(dot)- com. You can sign it at that link.

If you think you've already signed it, please comment below and we'll check.

@marjanasolajic marjanasolajic changed the title Inline methods that folds to a constant before analysis Inline methods that fold to a constant before the static analysis Dec 6, 2020
@marjanasolajic marjanasolajic force-pushed the plugin-inline-during-parsing branch 2 times, most recently from 35828cd to 2ecab68 Compare December 6, 2020 16:55
Native Image automation moved this from To do to Done Dec 7, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Native Image
  
Done
Development

Successfully merging this pull request may close these issues.

None yet

5 participants