Skip to content

Eliminate dead ICatch handlers#2321

Merged
mshinwell merged 9 commits intoocaml:trunkfrom
gretay-js:deadcode-catch-handler
Aug 6, 2019
Merged

Eliminate dead ICatch handlers#2321
mshinwell merged 9 commits intoocaml:trunkfrom
gretay-js:deadcode-catch-handler

Conversation

@gretay-js
Copy link
Copy Markdown
Contributor

This is an improvement of deadcode pass.
This PR detects dead handlers by propagating information about Iexit instructions that appear inside an instruction i and refer to handlers outside of i, using correct scoping rules depending on whether Icatch is recursive or not. The function deadcode was refactored to return a record instead of a pair.

Here is a common and simple example that resulted in dead code before this PR:

type t = A | B

let bar t (x:float) (y:float) = match t with
  | A -> x < y
  | B -> x > y

let foo t x y = not (bar t x y)

Generated assembly (amd64, compiled with flambda) with dead code in blocks L107 and L106:

camlTest1__foo_22:
        .cfi_startproc
.L113:
        cmpq    $1, %rax
        je      .L110
        movsd   (%rdi), %xmm0
        movsd   (%rbx), %xmm1
        comisd  %xmm0, %xmm1
        ja      .L112
        movq    $1, %rax
        jmp     .L111
        .align  4
.L112:
        xorq    %rax, %rax
.L111:
        leaq    1(%rax,%rax), %rax
        ret
        .align  4
.L110:
        movsd   (%rdi), %xmm0
        movsd   (%rbx), %xmm1
        comisd  %xmm1, %xmm0
        ja      .L109
        movq    $1, %rax
        jmp     .L108
        .align  4
.L109:
        xorq    %rax, %rax
.L108:
        leaq    1(%rax,%rax), %rax
        ret
        .align  4
.L107:
        movq    $3, %rax
        ret
        .align  4
.L106:
        movq    $1, %rax
        ret

@mshinwell
Copy link
Copy Markdown
Contributor

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 the following set of OPAM packages I measured 54 occurrences in total:

ocamlfind
num
coq
ocamlbuild
menhir
why3

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 match ... with ... exception is being used.

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.

@chambart chambart self-assigned this Jun 27, 2019
@chambart
Copy link
Copy Markdown
Contributor

This is not eliminating some unreachable handler in recursive cases, for instance:

catch exit 1
with 1 -> ()
 and 2 -> exit 3
 and 3 -> ()

I made a pull request on the original branch.

@gretay-js
Copy link
Copy Markdown
Contributor Author

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.

@gretay-js gretay-js force-pushed the deadcode-catch-handler branch from 064dc79 to 5d79f82 Compare July 3, 2019 08:15
@gretay-js
Copy link
Copy Markdown
Contributor Author

gretay-js commented Jul 3, 2019

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.

@shindere
Copy link
Copy Markdown
Contributor

shindere commented Jul 3, 2019 via email

@gretay-js
Copy link
Copy Markdown
Contributor Author

Yes absolutely, thanks. It'd just be nice to make the indentation similar to the rest of the file, if you don't mind.

@shindere I hope this version is better: 5929437

@shindere
Copy link
Copy Markdown
Contributor

shindere commented Jul 3, 2019 via email

Copy link
Copy Markdown
Contributor

@lthls lthls left a comment

Choose a reason for hiding this comment

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

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.


(* [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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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)

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. *)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

My thoughts about this algorithm (no action needed, though you can use this to add comments if you want):

  • live_exits records all the nfail numbers that have been passed to Cexit in reachable code from the current expression. This includes both free and bound continuations. Because of this, with duplicate nfail numbers 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 the live_exits of either the body or a handler that has just been found reachable. But since the initial value of live_exits in the fixpoint contains the ones from the next instruction (after the catch), reuse of nfail numbers 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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would make a special case for when used_handlers is empty, to avoid creating a catch handler with no handlers

@gretay-js
Copy link
Copy Markdown
Contributor Author

I'd like to see the issue of uniqueness settled

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.

@gretay-js
Copy link
Copy Markdown
Contributor Author

An updated version in 5be5511

  • does not rely on uniqueness of indexes
  • still collects dead handlers in recursive catch construct, as in the above example
  • live exits refer to correctly scoped handlers only, as in the original version
  • catch with no handlers is simplified

@gretay-js
Copy link
Copy Markdown
Contributor Author

Forgot to say that catch without used handlers occurs 11 times in the build of the compiler itself.

@gretay-js gretay-js force-pushed the deadcode-catch-handler branch from 5be5511 to f2d05a7 Compare July 8, 2019 18:13
@shindere
Copy link
Copy Markdown
Contributor

shindere commented Jul 8, 2019 via email

@gretay-js
Copy link
Copy Markdown
Contributor Author

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.

@shindere
Copy link
Copy Markdown
Contributor

shindere commented Jul 9, 2019 via email

@lthls
Copy link
Copy Markdown
Contributor

lthls commented Jul 9, 2019

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 Cmmgen) to:

catch
  4 - (* expression *)
with(n)
  3

(4 - x is the translation of not x, and 3 is true)

This is not optimal, but not a huge problem.

@gretay-js
Copy link
Copy Markdown
Contributor Author

Thank you for your fix, I've just merged it in on the PR branch.

let i =
match used_handlers with
| [] -> (* Simplify catch without handlers *)
patch_next body'.i s.i
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks! I added some code to handle this case.

{ i with desc = Icatch(rec_flag, handlers, body'.i); next = s.i }
in
{ i;
regs = i.live;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this needs to be regs = Reg.add_set_array i.live arg since i could now be an instruction with arguments.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, but I think we can just use regs = body'.regs in this case.

@lthls
Copy link
Copy Markdown
Contributor

lthls commented Jul 10, 2019

Apart from my last comments, I'm convinced about the correctness of this patch.
You need a Changes entry, and then assuming that CI is green we can merge.

@shindere
Copy link
Copy Markdown
Contributor

shindere commented Jul 11, 2019 via email

@gretay-js
Copy link
Copy Markdown
Contributor Author

All green on the CI.

@gretay-js
Copy link
Copy Markdown
Contributor Author

Can this be merged please?

Copy link
Copy Markdown
Contributor

@lthls lthls left a comment

Choose a reason for hiding this comment

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

I'm satisfied with the code. I'm slightly biased though, so I won't merge myself (unless it gets stalled for too long).

@mshinwell
Copy link
Copy Markdown
Contributor

We're going to do a final check on the Jane Street tree of this one, although we believe it to be correct.

Copy link
Copy Markdown
Member

@damiendoligez damiendoligez left a comment

Choose a reason for hiding this comment

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

Good to go when the JS check is green (and the conflict is fixed).

@gretay-js gretay-js force-pushed the deadcode-catch-handler branch from 61d2a28 to a657aa3 Compare July 17, 2019 10:11
@gretay-js
Copy link
Copy Markdown
Contributor Author

Thank you for the reviews. Testing on Jane code finished with no regressions. I've just updated Changes to fix the conflict.

@gretay-js
Copy link
Copy Markdown
Contributor Author

gretay-js commented Aug 5, 2019

Can this be merged please, it's ready.

@mshinwell mshinwell merged commit e08a968 into ocaml:trunk Aug 6, 2019
gretay-js added a commit to gretay-js/ocaml that referenced this pull request Aug 26, 2019
gretay-js added a commit to gretay-js/ocaml that referenced this pull request Nov 21, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants