-
Notifications
You must be signed in to change notification settings - Fork 15.2k
[analyzer] Workaround for slowdown spikes (unintended scope increase) #136720
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
Changes from 1 commit
011008b
d57946b
ba6a2ce
4140f53
6c238f8
32399ee
e8991c8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2523,6 +2523,20 @@ bool ExprEngine::replayWithoutInlining(ExplodedNode *N, | |
| return true; | ||
| } | ||
|
|
||
| /// Return the innermost location context which is inlined at `Node`, unless | ||
| /// it's the top-level (entry point) location context. | ||
| static const LocationContext *getInlinedLocationContext(ExplodedNode *Node, | ||
| ExplodedGraph &G) { | ||
| const LocationContext *CalleeLC = Node->getLocation().getLocationContext(); | ||
| const LocationContext *RootLC = | ||
balazs-benics-sonarsource marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| (*G.roots_begin())->getLocation().getLocationContext(); | ||
|
|
||
| if (CalleeLC->getStackFrame() == RootLC->getStackFrame()) | ||
| return nullptr; | ||
|
|
||
| return CalleeLC; | ||
| } | ||
balazs-benics-sonarsource marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| /// Block entrance. (Update counters). | ||
| void ExprEngine::processCFGBlockEntrance(const BlockEdge &L, | ||
| NodeBuilderWithSinks &nodeBuilder, | ||
|
|
@@ -2570,21 +2584,24 @@ void ExprEngine::processCFGBlockEntrance(const BlockEdge &L, | |
| const ExplodedNode *Sink = | ||
| nodeBuilder.generateSink(Pred->getState(), Pred, &tag); | ||
|
|
||
| // Check if we stopped at the top level function or not. | ||
| // Root node should have the location context of the top most function. | ||
| const LocationContext *CalleeLC = Pred->getLocation().getLocationContext(); | ||
| const LocationContext *CalleeSF = CalleeLC->getStackFrame(); | ||
| const LocationContext *RootLC = | ||
| (*G.roots_begin())->getLocation().getLocationContext(); | ||
| if (RootLC->getStackFrame() != CalleeSF) { | ||
| Engine.FunctionSummaries->markReachedMaxBlockCount(CalleeSF->getDecl()); | ||
| if (const LocationContext *LC = getInlinedLocationContext(Pred, G)) { | ||
| // FIXME: This will unconditionally prevent inlining this function (even | ||
| // from other entrypoints), which is not a reasonable heuristic: even if | ||
|
||
| // we reached max block count on this particular execution path, there | ||
| // may be other execution paths (especially with other parametrizations) | ||
| // where the analyzer can reach the end of the function (so there is no | ||
| // natural reason to avoid inlining it). However, disabling this would | ||
| // significantly increase the analysis time (because more entrypoints | ||
| // would exhaust their allocated budget), so it must be compensated by a | ||
| // different (more reasonable) reduction of analysis scope. | ||
| Engine.FunctionSummaries->markShouldNotInline( | ||
| LC->getStackFrame()->getDecl()); | ||
|
|
||
| // Re-run the call evaluation without inlining it, by storing the | ||
| // no-inlining policy in the state and enqueuing the new work item on | ||
| // the list. Replay should almost never fail. Use the stats to catch it | ||
| // if it does. | ||
| if ((!AMgr.options.NoRetryExhausted && | ||
| replayWithoutInlining(Pred, CalleeLC))) | ||
| if ((!AMgr.options.NoRetryExhausted && replayWithoutInlining(Pred, LC))) | ||
| return; | ||
| NumMaxBlockCountReachedInInlined++; | ||
| } else | ||
|
|
@@ -2856,8 +2873,29 @@ void ExprEngine::processBranch( | |
| // conflicts with the widen-loop analysis option (which is off by | ||
| // default). If we intend to support and stabilize the loop widening, | ||
| // we must ensure that it 'plays nicely' with this logic. | ||
| if (!SkipTrueBranch || AMgr.options.ShouldWidenLoops) | ||
| if (!SkipTrueBranch || AMgr.options.ShouldWidenLoops) { | ||
| Builder.generateNode(StTrue, true, PredN); | ||
| } else if (AMgr.options.LegacyInliningPrevention) { | ||
| // FIXME: There is an ancient and very arbitrary heuristic in | ||
| // `ExprEngine::processCFGBlockEntrance` which prevents all further | ||
| // inlining of a function if it finds an execution path within that | ||
| // function which reaches the `MaxBlockVisitOnPath` limit (a/k/a | ||
| // `analyzer-max-loop`, by default four iterations in a loop). Adding | ||
| // this "don't assume third iteration" logic significantly increased | ||
| // the analysis runtime on some inputs because less functions were | ||
| // arbitrarily excluded from being inlined, so more entrypoints used | ||
| // up their full allocated budget. As a hacky compensation for this, | ||
| // here we apply the "should not inline" mark in cases when the loop | ||
| // could potentially reach the `MaxBlockVisitOnPath` limit without the | ||
| // "don't assume third iteration" logic. This slightly overcompensates | ||
| // (activates if the third iteration can be entered, and will not | ||
| // recognize cases where the fourth iteration would't be completed), but | ||
| // should be good enough for practical purposes. | ||
| if (const LocationContext *LC = getInlinedLocationContext(Pred, G)) { | ||
| Engine.FunctionSummaries->markShouldNotInline( | ||
| LC->getStackFrame()->getDecl()); | ||
| } | ||
| } | ||
|
||
| } | ||
|
|
||
| if (StFalse) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,198 @@ | ||||||
| // RUN: %clang_analyze_cc1 -analyzer-checker=core,debug.ExprInspection -verify=expected,default %s | ||||||
| // RUN: %clang_analyze_cc1 -analyzer-checker=core,debug.ExprInspection -analyzer-config legacy-inlining-prevention=false -verify=expected,disabled %s | ||||||
|
|
||||||
| int getNum(void); // Get an opaque number. | ||||||
|
|
||||||
| void clang_analyzer_numTimesReached(void); | ||||||
| void clang_analyzer_dump(int arg); | ||||||
|
|
||||||
| //----------------------------------------------------------------------------- | ||||||
| // Simple case: inlined function never reaches `analyzer-max-loop`. | ||||||
|
|
||||||
| int inner_simple(void) { | ||||||
| clang_analyzer_numTimesReached(); // expected-warning {{2}} | ||||||
| return 42; | ||||||
| } | ||||||
|
|
||||||
| int outer_simple(void) { | ||||||
| int x = inner_simple(); | ||||||
| int y = inner_simple(); | ||||||
| return 53 / (x - y); // expected-warning {{Division by zero}} | ||||||
| } | ||||||
|
|
||||||
| //----------------------------------------------------------------------------- | ||||||
| // Inlined function always reaches `analyzer-max-loop`. | ||||||
|
|
||||||
| int inner_fixed_loop_1(void) { | ||||||
| int i; | ||||||
| clang_analyzer_numTimesReached(); // expected-warning {{1}} | ||||||
|
||||||
| for (i = 0; i < 10; i++); | ||||||
| clang_analyzer_numTimesReached(); // no-warning | ||||||
|
||||||
| clang_analyzer_numTimesReached(); // no-warning | |
| clang_analyzer_numTimesReached(); // FIXME: It should be reachable |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would be careful with this FIXME -- if somebody follows it and fixes this issue (e.g. by enabling unroll-loops by default) we could get yet another large slowdown when less loops hit the analyzer-max-loop limit, less functions end on the "don't inline" list and more entry points spend their budget.
Obviously in an ideal world an empty loop like this shouldn't stop the analyzer – but we must replace this awkward loop-based inlining restriction with a more robust heuristic before we can do that. (I don't want to spread "older dumber analyzer would give up here, don't inline this function" hacks in the codebase as we improve the loop handling.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't get the argument why would be a risk of having a FIXME here. It's the reviewer's and maintainer's responsibility to ensure the changes align with the long term directions of the project.
If someone would just override analyzer-max-loop for this test "to fix the FIXME", that should be rejected.
However, if we found a way to enable it by default, or have a smarter engine, then it's fine - but that would also touch files beyond this single test file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO the situation is complex because:
- from the POV of the users this "analysis is stopped by long loops" behavior is obviously a bug that needs to be fixed;
- but from the POV of the inlining prevention heuristic (which is tested here) this is a known feature (or at least "property") of the analyzer engine which is important for the correct delineation of the scope.
You're right that these deserve a FIXME, but I'll probably write a longer FIXME block that explains the full situation instead of short inline FIXME notes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good.
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should have comments here that first it's inlined, second it's not because of the given heuristic.
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My problem with these no-warnings in general in this PR that it documents what the test currently does, but what they should document what the tests should/could expect.
In this case in an ideal world, we should actually get a diagnostic, thus the desired outcome is not a no-warning.
Consequently, a FIXME would be more appropriate I think.
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FIXME.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: I'd remove "dumb" from the user facing string.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, I was annoyed by the situation when I wrote this description, but I was already planning to rewrite these before finalizing the commit.
EDIT: done.