Conversation
|
I've asked @chambart and @lthls if one of them can review this. I've been doing some experiments to determine how often dead code, that this patch removes, occurs. On some packages there are no occurrences (e.g. in the compiler itself). So the situation doesn't happen "very often", but it definitely happens a fair amount, probably mostly when It's good to delete these pieces of dead code not only for code size reasons but also so analysis tools that spot dead code don't complain. On one particular Jane Street executable there were nearly 4,000 dead basic blocks that such a tool spotted and were subsequently removed by this patch. The parts of the patch that change a tupled return type to a record could potentially be committed as a separate no-op commit. |
|
This is not eliminating some unreachable handler in recursive cases, for instance: I made a pull request on the original branch. |
|
Thank you for the review and the improvements - I saw the PR! Let me try to construct a cmm test from your example. @mshinwell says we can't currently generate that example in the compiler. It only generates a single exist for recursive catch, but with flambda2 it will be possible. |
064dc79 to
5d79f82
Compare
|
Here is an improved version (@chambart thanks!), rebased. I added a test and added a comment explaining the invariant it relies on. @chambart : I hope you don't mind that I changed variable name "exit" to "nfail" as in other places, as "exit" confuses my syntax highlighting. It wasn't hard to construct a cmm test for the example above of recursive catch with dead handlers. The problem was to convince ocamltest to do the intended checks. I had to make a small addition to ocamltest. @shindere, I couldn't find a way around adding "flags" and "output" to codegen actions in ocamltest. Does the change in 5d79f82 look right? I implemented a separate function to check the variants we rely on for elimination of dead handlers: the indexes of handlers are unique across the entire function and Iexit instructions refer to the correctly scoped handlers. I ran it on all the code in the compiler and a few other tests. The only place where invariants didn't hold is in the "asmgen" testsuite. I fixed it as part of adding the new test in commit. The fix involves a change in parsecmm of while loops and exits 5d79f82. @lthls can you please have a look? It apppears to have been slighly broken for a while. I wonder if it is worth including the invariant checks in the compiler, perhaps under a separate flag. It is similar to linear_invariants check that @lthls has in another PR. These test can be useful especially with flambda2 changes coming up. |
|
Greta Yorsh (2019/07/03 01:28 -0700):
The problem was to convince ocamltest to do the intended checks. I
had to make a small addition to ocamltest. @shindere, I couldn't find
a way around adding "flags" and "output" to codegen actions in
ocamltest. Does the change in 5d79f82 look right?
Yes absolutely, thanks. It'd just be nice to make the indentation
similar to the rest of the file, if you don't mind.
I implemented a separate function to check the variants we rely on for elimination of dead handlers: the indexes of handlers are unique across the entire function and Iexit instructions refer to the correctly scoped handlers.
I ran it on all the code in the compiler and a few other tests. The only place where invariants didn't hold is in the "asmgen" testsuite. I fixed it as part of adding the new test in commit. The fix involves a change in parsecmm of while loops and exits 5d79f82. @lthls can you please have a look? It apppears to have been slighly broken for a while.
I wonder if it is worth including the invariant checks in the
compiler, perhaps under a separate flag. It is similar to
linear_invariants check that @lthls has in another PR. These test can
be useful especially with flambda2 changes coming up.
I think @xavierleroy may want to comment, here.
|
lthls
left a comment
There was a problem hiding this comment.
The handling of while in parsecmm.mly seems indeed to have been broken for a very long time, and I have to admit that I'm partly guilty in that I didn't notice it was wrong when I last patched it. Thanks for fixing it.
About the uniqueness of nfail identifiers, I think it's a good thing to have, but I'm wary of relying on it without enforcing it explicitly. I have an open PR (#1400) that can help with enforcement, or you could commit your own check if you prefer. As noted in an inline comment, if nfail numbers are not unique within a function declaration then reachable handlers could be dropped, leading to strange errors later in the backend.
I've also had a look at your initial version for Icatch, and while @chambart 's one allows for more aggressive deadcode elimination (in theory, at least), the original implementation had the nice property that the live_exits set corresponded to free continuations only. You could probably restore this property with a small patch, but I'm not sure if it is important enough.
I'd like to see the issue of uniqueness settled before approving, but otherwise this PR looks good to me.
asmcomp/deadcode.ml
Outdated
|
|
||
| (* [deadcode i] returns a pair of an optimized instruction [i'] | ||
| and a set of registers live "before" instruction [i]. *) | ||
| module Int = Identifiable.Make (Numbers.Int) |
There was a problem hiding this comment.
Numbers.Int's signature already includes Identifiable.S, so the call to Identifiable.Make seems redundant (@chambart tells me it's probably an oversight on his part)
asmcomp/deadcode.ml
Outdated
| let handlers' = Int.Map.map deadcode (Int.Map.of_list handlers) in | ||
| (* Previous passes guarantee that indexes of handlers are unique | ||
| across the entire function and Iexit instructions refer | ||
| to the correctly scoped handlers. *) |
There was a problem hiding this comment.
My thoughts about this algorithm (no action needed, though you can use this to add comments if you want):
live_exitsrecords all thenfailnumbers that have been passed toCexitin reachable code from the current expression. This includes both free and bound continuations. Because of this, with duplicatenfailnumbers in a nested context, the outer handler could be considered reachable even though it is not.- handlers are added to
used_handlers(thus considered reachable) only when they are found for the first time in thelive_exitsof either the body or a handler that has just been found reachable. But since the initial value oflive_exitsin the fixpoint contains the ones from the next instruction (after the catch), reuse ofnfailnumbers could lead to a reachable handler that gets dropped because an exit to a different handler with the same number occurs in the next instruction. - nonrecursive handlers are handled the same way as recursive ones, which is fine in this context, but again if duplicate numbers were allowed this could make a handler wrongly considered reachable.
| in | ||
| let live_exits, used_handlers = | ||
| Int.Set.fold add_live body'.exits (s.exits, []) | ||
| in |
There was a problem hiding this comment.
I would make a special case for when used_handlers is empty, to avoid creating a catch handler with no handlers
Uniqueness can be checked with a small change to the current version. Actually, I have already had a version of just that, but I removed it after convincing myself that the indexes are unique. I can resurrect it. The invariant check are on a separate branch at the moment: 8cea3a6. They are subsumed by your PR #1400, except that mine version is on Mach instead of Cmm and checks for dead handlers. The problem in asmgen tests is probably the same one you noticed in #1400, as it had duplicated handlers. Thank you for your other suggestions, I'll address them at the same time, but I won't be able to do it until Monday, sorry. |
|
An updated version in 5be5511
|
|
Forgot to say that catch without used handlers occurs 11 times in the build of the compiler itself. |
5be5511 to
f2d05a7
Compare
|
Greta Yorsh (2019/07/08 11:12 -0700):
Forgot to say that catch without used handlers occurs 11 times in the
build of the compiler itself.
Sorry, not sure what this means / how to interprete this information? Is
that something that should be fixed somehow?
|
No, on the contrary. This was just to say that the optimization is useful: the simplification is indeed triggered. |
|
Greta Yorsh (2019/07/09 01:05 -0700):
No, on the contrary. This was just to say that the optimization is
useful: the simplification is indeed triggered.
Okay it makes sense now, many thanks for having taken the time to
clarify!
|
|
So, removing empty handlers turns out to be not as easy as I expected (this is the cause of the CI failures). I'm sending a small pull request with a fix, which I hope is correct, but the safe solution may be to simply forget about this simplification and generate catches with no handlers anyway. @shindere catch without used handlers are uncommon, but can happen in the following case: (false || not (* some non-trivial expression *))This gets compiled (in ( This is not optimal, but not a huge problem. |
|
Thank you for your fix, I've just merged it in on the PR branch. |
asmcomp/deadcode.ml
Outdated
| let i = | ||
| match used_handlers with | ||
| | [] -> (* Simplify catch without handlers *) | ||
| patch_next body'.i s.i |
There was a problem hiding this comment.
I just noticed that my patch doesn't work if the body of the catch is Iend. I hope it never happens, and a quick test on the compiler sources and testsuite doesn't trigger anything, but adding assert (body'.i.desc <> Iend); or a small if to handle this case would be nice.
There was a problem hiding this comment.
Thanks! I added some code to handle this case.
asmcomp/deadcode.ml
Outdated
| { i with desc = Icatch(rec_flag, handlers, body'.i); next = s.i } | ||
| in | ||
| { i; | ||
| regs = i.live; |
There was a problem hiding this comment.
I think this needs to be regs = Reg.add_set_array i.live arg since i could now be an instruction with arguments.
There was a problem hiding this comment.
Yes, but I think we can just use regs = body'.regs in this case.
|
Apart from my last comments, I'm convinced about the correctness of this patch. |
|
Before this gets merged, it may be a good idea to cleanup the history.
|
|
All green on the CI. |
|
Can this be merged please? |
lthls
left a comment
There was a problem hiding this comment.
I'm satisfied with the code. I'm slightly biased though, so I won't merge myself (unless it gets stalled for too long).
|
We're going to do a final check on the Jane Street tree of this one, although we believe it to be correct. |
damiendoligez
left a comment
There was a problem hiding this comment.
Good to go when the JS check is green (and the conflict is fixed).
61d2a28 to
a657aa3
Compare
|
Thank you for the reviews. Testing on Jane code finished with no regressions. I've just updated Changes to fix the conflict. |
|
Can this be merged please, it's ready. |
This is an improvement of
deadcodepass.This PR detects dead handlers by propagating information about
Iexitinstructions that appear inside an instructioniand refer to handlers outside ofi, using correct scoping rules depending on whetherIcatchis recursive or not. The functiondeadcodewas refactored to return a record instead of a pair.Here is a common and simple example that resulted in dead code before this PR:
Generated assembly (amd64, compiled with flambda) with dead code in blocks L107 and L106: