-
Notifications
You must be signed in to change notification settings - Fork 1.9k
C++: Remove unreachable IR #648
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
C++: Remove unreachable IR #648
Conversation
The AST dataflow library essentially ignores conversions, which is probably the right behavior. Converting an `int` to a `long` preserves the value, even if the bit pattern might be different. It's arguable whether narrowing conversions should be treated as dataflow, but we'll do so for now. We can revisit that if we see it cause problems.
This fixes a subtle bug in the construction of aliased SSA. `getResultMemoryAccess` was failing to return a `MemoryAccess` for a store to a variable whose address escaped. This is because no `VirtualIRVariable` was being created for such variables. The code was assuming that any access to such a variable would be via `UnknownMemoryAccess`. The result is that accesses to such variables were not being modeled in SSA at all. Instead, the way to handle this is to have a `VariableMemoryAccess` even when the variable being accessed has escaped, and to have `VariableMemoryAccess::getVirtualVariable()` return the `UnknownVirtualVariable` for escaped variables. In the future, this will also let us be less conservative about inserting `Chi` nodes, because we'll be able to determine that there's an exact overlap between two accesses to the same escaped variable in some cases.
This commit adds a new model interface that describes the known side effects (or lack thereof) of a library function. Does it read memory, does it write memory, and do any of its parameters escape? Initially, we have models for just two Standard Library functions: `std::move` and `std::forward`, which neither read nor write memory, and do not escape their parameter. IR construction has been updated to insert the correct side effect instruction (or no side effect instruction) based on the model.
Made `Node::getType()`, `Node::asParameter()`, and `Node::asUninitialized()` operate directly on the IR. This actually fixed several diffs compared to the AST dataflow, because `getType()` wasn't holding for nodes that weren't `Exprs`. Made `Uninitialized` a `VariableInstruction`. This makes it consistent with `InitializeParameter`.
I've separated the model interface for memory side effects from the model for escaped addresses. It will be fairly common for a given model to extend both interfaces, but they are used for two different purposes. I've also put each model interface and the non-member predicates that query it into a named module, which seemed cleaner than having predicates named `functionModelReadsMemory()` and `getFunctionModelParameterAliasBehavior()`.
This sort of fixes one FP and causes a new FN, but for the wrong reasons. The IR dataflow is tracking the reference itself, rather than the referred-to object. Once we can better model indirections, we can make this work correctly. This change is still the right thing to do, because it ensures that the dataflow is looking at actual expression being computed by the instruction.
jbj
left a comment
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.
Otherwise LGTM. The CFG pruning for the AST-based CFG is notoriously slow for C/C++, so I'm glad to see that this version is much less ambitious. I'd still like to know if it performs okay and whether it's important to have three copies.
|
|
||
| module Graph { | ||
| predicate isEntryBlock(ReachableBlock block) { | ||
| block = block.getFunctionIR().getEntryBlock() |
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.
The optimiser is often unlucky on joins like this one. Could block.getFunctionIR() be replaced with any(FunctionIR f)?
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.
Done
| binInstr instanceof SubInstruction and result = sub(left, right) or | ||
| binInstr instanceof MulInstruction and result = mul(left, right) or | ||
| binInstr instanceof DivInstruction and result = div(left, right) | ||
| ) |
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.
For a constant analysis that's used for reachability, isn't it even more important to support >, ==, and so on?
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've added support for equality and relational operators.
| } | ||
|
|
||
| predicate isBlockReachable(IRBlock block) { | ||
| getAFeasiblePredecessor*(block) = block.getFunctionIR().getEntryBlock() |
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.
The optimiser is often unlucky on joins like this one. Could block.getFunctionIR() be replaced with any(FunctionIR f)?
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.
Done
| private import NewIR | ||
|
|
||
| private class OldBlock = Reachability::ReachableBlock; | ||
| private class OldInstruction = Reachability::ReachableInstruction; |
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.
Do the benefits outweigh the cost of running the reachability and constant analyses three times? There will certainly be cases where it becomes better when iterated, but it's also hundreds of lines of extra code we'll be running every time. Using pyrameterized modules also comes with a maintainability cost.
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 actually only run it twice: Once on raw IR (used when building unaliased_ssa), and once on unaliased_ssa (used when building aliased_ssa). I've now removed the aliased_ssa instantiation, since it was unused.
This reverts commit df882a9.
This change moves the simple constant analysis that was used by the const_func test into a pyrameterized module for use on any stage of the IR. This will be used to detect unreachable code.
This change removes any IR instructions that can be statically proven unreachable. To detect unreachable IR, we first run a simple constant value analysis on the IR. Then, any `ConditionalBranch` with a constant condition has the appropriate edge marked as "infeasible". We define a class `ReachableBlock` as any `IRBlock` with a path from the entry block of the function. SSA construction has been modified to operate only on `ReachableBlock` and `ReachableInstruction`, which ensures that only reachable IR gets translated into SSA form. For any infeasible edge where its predecessor block is reachable, we replace the original target of the branch with an `Unreached` instruction, which lets us preserve the invariant that all `ConditionalBranch` instructions have both a true and a false edge, and allows guard inference to still work. The changes to `SSAConstruction.qll` are not as scary as they look. They are almost entirely a mechanical replacement of `OldIR::IRBlock` with `OldBlock`, which is just an alias for `ReachableBlock`. Note that the `constant_func.ql` test can determine that the two new test functions always return 0. Removing unreachable code helps get rid of some common FPs in IR-based dataflow analysis, especially for constructs like `while(true)`.
We never actually consumed this iteration, since SSA construction only depends on the reachability instantiation of the previous IR layer.
74dc8a7 to
4170d4f
Compare
|
I believe I've addressed all feedback. |
|
LGTM. Now we're just waiting for Jenkins and #597. |
|
Looks like test output needs to be updated. |
rdmarsh2
left a comment
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.
Updated test expectations; no code change
Note: This PR depends on PR #597. Once that PR is merged, most of the commits in this PR will disappear. I suggest reviewing only the most recent commits in this PR, starting with bcf5cac.
This change removes any IR instructions that can be statically proven unreachable. To detect unreachable IR, we first run a simple constant value analysis on the IR. Then, any
ConditionalBranchwith a constant condition has the appropriate edge marked as "infeasible". We define a classReachableBlockas anyIRBlockwith a path from the entry block of the function. SSA construction has been modified to operate only onReachableBlockandReachableInstruction, which ensures that only reachable IR gets translated into SSA form. For any infeasible edge where its predecessor block is reachable, we replace the original target of the branch with anUnreachedinstruction, which lets us preserve the invariant that allConditionalBranchinstructions have both a true and a false edge, and allows guard inference to still work.The changes to
SSAConstruction.qllare not as scary as they look. They are almost entirely a mechanical replacement ofOldIR::IRBlockwithOldBlock, which is just an alias forReachableBlock.Note that the
constant_func.qltest can determine that the two new test functions always return 0.Removing unreachable code helps get rid of some common FPs in IR-based dataflow analysis, especially for constructs like
while(true).