Skip to content

Commit

Permalink
Add a generic backward dataflow analyzer and use it for liveness anal…
Browse files Browse the repository at this point in the history
…ysis (ocaml#10404)

The analyzer is parameterized by an abstract domain and a transfer function.

For recursive handlers, it remembers the latest inferred abstract state
and uses it to start the next fixpoint iteration.  This avoids behaviors
exponential in the nesting of recursive handlers, like we would have
if we started every iteration with bottom.

This exponential behavior was present in the old implementation of liveness
analysis.  It is gone in the new implementation that just calls into
the generic analyzer.
  • Loading branch information
xavierleroy authored and Nicolas Chataing committed May 20, 2021
1 parent 9915ccb commit 1dc931e
Show file tree
Hide file tree
Showing 7 changed files with 222 additions and 103 deletions.
14 changes: 12 additions & 2 deletions .depend
Expand Up @@ -2563,6 +2563,16 @@ asmcomp/comballoc.cmx : \
asmcomp/comballoc.cmi
asmcomp/comballoc.cmi : \
asmcomp/mach.cmi
asmcomp/dataflow.cmo : \
asmcomp/mach.cmi \
asmcomp/cmm.cmi \
asmcomp/dataflow.cmi
asmcomp/dataflow.cmx : \
asmcomp/mach.cmx \
asmcomp/cmm.cmx \
asmcomp/dataflow.cmi
asmcomp/dataflow.cmi : \
asmcomp/mach.cmi
asmcomp/deadcode.cmo : \
asmcomp/reg.cmi \
asmcomp/proc.cmi \
Expand Down Expand Up @@ -2735,15 +2745,15 @@ asmcomp/liveness.cmo : \
asmcomp/printmach.cmi \
utils/misc.cmi \
asmcomp/mach.cmi \
asmcomp/cmm.cmi \
asmcomp/dataflow.cmi \
asmcomp/liveness.cmi
asmcomp/liveness.cmx : \
asmcomp/reg.cmx \
asmcomp/proc.cmx \
asmcomp/printmach.cmx \
utils/misc.cmx \
asmcomp/mach.cmx \
asmcomp/cmm.cmx \
asmcomp/dataflow.cmx \
asmcomp/liveness.cmi
asmcomp/liveness.cmi : \
asmcomp/mach.cmi
Expand Down
12 changes: 8 additions & 4 deletions Changes
Expand Up @@ -90,6 +90,11 @@ Working version

### Code generation and optimizations:

- #1400: Add an optional invariants check on Cmm, which can be activated
with the -dcmm-invariants flag
(Vincent Laviron, with help from Sebastien Hinderer, review by Stephen Dolan
and David Allsopp)

- #9876: do not cache the young_limit GC variable in a processor register.
This affects the ARM64, PowerPC and RISC-V ports, making signal handling
and minor GC triggers more reliable, at the cost of a small slowdown.
Expand All @@ -111,10 +116,9 @@ Working version
- #10349: Fix destroyed_at_c_call on RISC-V
(Mark Shinwell, review by Nicolás Ojeda Bär)

- #1400: Add an optional invariants check on Cmm, which can be activated
with the -dcmm-invariants flag
(Vincent Laviron, with help from Sebastien Hinderer, review by Stephen Dolan
and David Allsopp)
- #10404: Add a generic backward dataflow analyzer and use it to speed up
liveness analysis
(Xavier Leroy, review by Gabriel Scherer, Greta Yorsh, Mark Shinwell)

### Type system:

Expand Down
86 changes: 86 additions & 0 deletions asmcomp/dataflow.ml
@@ -0,0 +1,86 @@
(**************************************************************************)
(* *)
(* OCaml *)
(* *)
(* Xavier Leroy, projet Cambium, INRIA Paris *)
(* *)
(* Copyright 2021 Institut National de Recherche en Informatique et *)
(* en Automatique. *)
(* *)
(* All rights reserved. This file is distributed under the terms of *)
(* the GNU Lesser General Public License version 2.1, with the *)
(* special exception on linking described in the file LICENSE. *)
(* *)
(**************************************************************************)

open Mach

module type DOMAIN = sig
type t
val bot: t
val join: t -> t -> t
val lessequal: t -> t -> bool
end

module Backward(D: DOMAIN) = struct

let analyze ?(exnhandler = fun x -> x) ~transfer instr =
let lbls =
(Hashtbl.create 20 : (int, D.t) Hashtbl.t) in
let get_lbl n =
match Hashtbl.find_opt lbls n with None -> D.bot | Some b -> b
and set_lbl n x =
Hashtbl.replace lbls n x in

let rec before end_ exn i =
match i.desc with
| Iend ->
transfer i ~next:end_ ~exn
| Ireturn | Iop (Itailcall_ind | Itailcall_imm _) ->
transfer i ~next:D.bot ~exn:D.bot
| Iop _ ->
let bx = before end_ exn i.next in
transfer i ~next:bx ~exn
| Iifthenelse(_, ifso, ifnot) ->
let bx = before end_ exn i.next in
let b1 = before bx exn ifso
and b0 = before bx exn ifnot in
transfer i ~next:(D.join b1 b0) ~exn
| Iswitch(_, cases) ->
let bx = before end_ exn i.next in
let b1 =
Array.fold_left
(fun accu case -> D.join accu (before bx exn case))
D.bot cases in
transfer i ~next:b1 ~exn
| Icatch(rc, handlers, body) ->
let bx = before end_ exn i.next in
begin match rc with
| Cmm.Nonrecursive ->
List.iter
(fun (n, h) -> set_lbl n (before bx exn h))
handlers
| Cmm.Recursive ->
let update changed (n, h) =
let b0 = get_lbl n in
let b1 = before bx exn h in
if D.lessequal b1 b0 then changed else (set_lbl n b1; true) in
while List.fold_left update false handlers do () done
end;
let b = before bx exn body in
transfer i ~next:b ~exn
| Iexit n ->
transfer i ~next:(get_lbl n) ~exn
| Itrywith(body, handler) ->
let bx = before end_ exn i.next in
let bh = exnhandler (before bx exn handler) in
let bb = before bx bh body in
transfer i ~next:bb ~exn
| Iraise _ ->
transfer i ~next:D.bot ~exn
in
let b = before D.bot D.bot instr in
(b, get_lbl)

end
85 changes: 85 additions & 0 deletions asmcomp/dataflow.mli
@@ -0,0 +1,85 @@
(**************************************************************************)
(* *)
(* OCaml *)
(* *)
(* Xavier Leroy, projet Cambium, INRIA Paris *)
(* *)
(* Copyright 2021 Institut National de Recherche en Informatique et *)
(* en Automatique. *)
(* *)
(* All rights reserved. This file is distributed under the terms of *)
(* the GNU Lesser General Public License version 2.1, with the *)
(* special exception on linking described in the file LICENSE. *)
(* *)
(**************************************************************************)

(* An abstract domain for dataflow analysis. Defines a type [t]
of abstractions, with lattice operations. *)

module type DOMAIN = sig
type t
val bot: t
val join: t -> t -> t
val lessequal: t -> t -> bool
end

(* Build a backward dataflow analysis engine for the given domain. *)

module Backward(D: DOMAIN) : sig

val analyze: ?exnhandler: (D.t -> D.t) ->
transfer: (Mach.instruction -> next: D.t -> exn: D.t -> D.t) ->
Mach.instruction ->
D.t * (int -> D.t)

(* [analyze ~exnhandler ~transfer instr] performs a backward dataflow
analysis on the Mach instruction [instr], typically a function body.
It returns a pair of
- the abstract state at the function entry point;
- a mapping from catch handler label to the abstract state at the
beginning of the handler with this label.
The [transfer] function is called as [transfer i ~next ~exn].
- [i] is a sub-instruction of [instr].
- [next] is the abstract state "after" the instruction for
normal control flow, falling through the successor(s) of [i].
- [exn] is the abstract state "after" the instruction for
exceptional control flow, branching to the nearest exception handler
or exiting the function with an unhandled exception.
The [transfer] function, then, returns the abstract state "before"
the instruction. The dataflow analysis will, then, propagate this
state "before" as the state "after" the predecessor instructions.
For compound instructions like [Iifthenelse], the [next] abstract
value that is passed to [transfer] is not the abstract state at
the end of the compound instruction (e.g. after the "then" and "else"
branches have joined), but the join of the abstract states at
the beginning of the sub-instructions. More precisely:
- for [Iifthenelse(tst, ifso, ifnot)], it's the join of the
abstract states at the beginning of the [ifso] and [ifnot]
branches;
- for [Iswitch(tbl, cases)], it's the join of the abstract states
at the beginning of the [cases] branches;
- for [Icatch(recflag, body, handlers)] and [Itrywith(body, handler)],
it's the abstract state at the beginning of [body].
The [transfer] function is called for every sub-instruction of [instr],
as many times as needed to reach a fixpoint. Hence, it can record
the results of the analysis at each sub-instruction in a mutable
data structure. For instance, the transfer function for liveness
analysis updates the [live] fields of instructions as a side
effect.
The optional [exnhandler] argument deals with exception handlers.
This is a function that transforms the abstract state at the
beginning of an exception handler into the exceptional abstract
state for the instructions within the body of the handler.
Typically, for liveness analysis, it takes the registers live at
the beginning of the handler and removes the register
[Proc.loc_exn_bucket] that carries the exception value. If not
specified, [exnhandler] defaults to the identity function.
*)

end
126 changes: 30 additions & 96 deletions asmcomp/liveness.ml
Expand Up @@ -18,126 +18,60 @@

open Mach

let live_at_exit = ref []
module Domain = struct
type t = Reg.Set.t
let bot = Reg.Set.empty
let join = Reg.Set.union
let lessequal = Reg.Set.subset
end

let find_live_at_exit k =
try
List.assoc k !live_at_exit
with
| Not_found -> Misc.fatal_error "Liveness.find_live_at_exit"
module Analyzer = Dataflow.Backward(Domain)

let live_at_raise = ref Reg.Set.empty

let rec live i finally =
(* finally is the set of registers live after execution of the
instruction sequence.
The result of the function is the set of registers live just
before the instruction sequence.
The instruction i is annotated by the set of registers live across
the instruction. *)
let transfer i ~next ~exn =
match i.desc with
Iend ->
i.live <- finally;
finally
| Ireturn | Iop(Itailcall_ind) | Iop(Itailcall_imm _) ->
i.live <- Reg.Set.empty; (* no regs are live across *)
Reg.set_of_array i.arg
| Iop op ->
let after = live i.next finally in
if operation_is_pure op (* no side effects *)
&& Reg.disjoint_set_array after i.res (* results are not used after *)
&& not (Proc.regs_are_volatile i.arg) (* no stack-like hard reg *)
&& not (Proc.regs_are_volatile i.res) (* is involved *)
if operation_is_pure op (* no side effects *)
&& Reg.disjoint_set_array next i.res (* results are not used after *)
&& not (Proc.regs_are_volatile i.arg) (* no stack-like hard reg *)
&& not (Proc.regs_are_volatile i.res) (* is involved *)
then begin
(* This operation is dead code. Ignore its arguments. *)
i.live <- after;
after
i.live <- next;
next
end else begin
let across_after = Reg.diff_set_array after i.res in
let across1 = Reg.diff_set_array next i.res in
let across =
(* Operations that can raise an exception (function calls,
bounds checks, allocations) can branch to the
nearest enclosing try ... with.
Hence, everything that must be live at the beginning of
the exception handler must also be live across this instr. *)
if operation_can_raise op
then Reg.Set.union across_after !live_at_raise
else across_after in
then Reg.Set.union across1 exn
else across1 in
i.live <- across;
Reg.add_set_array across i.arg
end
| Iifthenelse(_test, ifso, ifnot) ->
let at_join = live i.next finally in
let at_fork = Reg.Set.union (live ifso at_join) (live ifnot at_join) in
i.live <- at_fork;
Reg.add_set_array at_fork i.arg
| Iswitch(_index, cases) ->
let at_join = live i.next finally in
let at_fork = ref Reg.Set.empty in
for i = 0 to Array.length cases - 1 do
at_fork := Reg.Set.union !at_fork (live cases.(i) at_join)
done;
i.live <- !at_fork;
Reg.add_set_array !at_fork i.arg
| Icatch(rec_flag, handlers, body) ->
let at_join = live i.next finally in
let aux (nfail,handler) (nfail', before_handler) =
assert(nfail = nfail');
let before_handler' = live handler at_join in
nfail, Reg.Set.union before_handler before_handler'
in
let aux_equal (nfail, before_handler) (nfail', before_handler') =
assert(nfail = nfail');
Reg.Set.equal before_handler before_handler'
in
let live_at_exit_before = !live_at_exit in
let rec fixpoint before_handlers =
live_at_exit := before_handlers @ !live_at_exit;
let before_handlers' = List.map2 aux handlers before_handlers in
live_at_exit := live_at_exit_before;
match rec_flag with
| Cmm.Nonrecursive ->
before_handlers'
| Cmm.Recursive ->
if List.for_all2 aux_equal before_handlers before_handlers'
then before_handlers'
else fixpoint before_handlers'
in
let init_state =
List.map (fun (nfail, _handler) -> nfail, Reg.Set.empty) handlers
in
let before_handler = fixpoint init_state in
(* We could use handler.live instead of Reg.Set.empty as the initial
value but we would need to clean the live field before doing the
analysis (to remove remnants of previous passes). *)
live_at_exit := before_handler @ !live_at_exit;
let before_body = live body at_join in
live_at_exit := live_at_exit_before;
i.live <- before_body;
before_body
| Iexit nfail ->
let this_live = find_live_at_exit nfail in
i.live <- this_live ;
this_live
| Itrywith(body, handler) ->
let at_join = live i.next finally in
let before_handler = live handler at_join in
let saved_live_at_raise = !live_at_raise in
live_at_raise := Reg.Set.remove Proc.loc_exn_bucket before_handler;
let before_body = live body at_join in
live_at_raise := saved_live_at_raise;
i.live <- before_body;
before_body
| Iifthenelse _
| Iswitch _ ->
i.live <- next;
Reg.add_set_array next i.arg
| Iend | Icatch _ | Iexit _ | Itrywith _ ->
i.live <- next;
next
| Iraise _ ->
i.live <- !live_at_raise;
Reg.add_set_array !live_at_raise i.arg
i.live <- exn;
Reg.add_set_array exn i.arg

let reset () =
live_at_raise := Reg.Set.empty;
live_at_exit := []
let exnhandler before_handler =
Reg.Set.remove Proc.loc_exn_bucket before_handler

let fundecl f =
let initially_live = live f.fun_body Reg.Set.empty in
let (initially_live, _) =
Analyzer.analyze ~exnhandler ~transfer f.fun_body in
(* Sanity check: only function parameters can be live at entrypoint *)
let wrong_live = Reg.Set.diff initially_live (Reg.set_of_array f.fun_args) in
if not (Reg.Set.is_empty wrong_live) then begin
Expand Down
1 change: 0 additions & 1 deletion asmcomp/liveness.mli
Expand Up @@ -16,5 +16,4 @@
(* Liveness analysis.
Annotate mach code with the set of regs live at each point. *)

val reset : unit -> unit
val fundecl: Mach.fundecl -> unit

0 comments on commit 1dc931e

Please sign in to comment.