Skip to content

Commit 2c6629c

Browse files
committed
Walk the occur fast paths without allocating
Why: The bounded walks that `occur` and `deep_occur` try first allocated a ref for their budget and a closure for the walk on every call, and `iter_type_expr` allocated another closure at each arrow type they visited. Unification runs `occur` for every variable it links, mostly on small types, so this allocation was a large share of what the fast paths cost. `occur_reaches` and `deep_occur_fast_absent` now walk the type with mutually recursive functions that take the budget left and return what remains, following the edges of `iter_type_expr` written out by constructor. The match on `type_desc` lists every constructor, so a new one is a compile error here rather than a missed edge. Only `deep_occur_fast_absent` at a `Tvariant` still goes through `iter_type_expr` with a closure, for the row's fields. The nodes visited, the order, the budget and the exceptions raised are unchanged. Measured with scripts/compilerBenchmarks.js against the previous commit: allocated words -1.03% in total (-1.14% without the project builds), stress/TypeAliasChain.res -8.3%, Belt map and set modules -2.7% to -3.4%. On a 552 module React app (clean build, one domain) the build allocates 0.99% fewer words. Keeping the closures and only skipping the walk for a leaf type (a variable or a constructor without arguments) gave -0.57% in total and -0.54% on the app. Review: Can a result change? No: a walk that finishes visited exactly the nodes the closure version visited, within the same budget; one that gives up still falls back to the full check. Signed-off-by: Jono Prest <jjprest@gmail.com>
1 parent cdafd94 commit 2c6629c

1 file changed

Lines changed: 67 additions & 31 deletions

File tree

‎compiler/ml/ctype.ml‎

Lines changed: 67 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1423,27 +1423,41 @@ exception Give_up
14231423

14241424
let walk_budget = 256
14251425

1426-
let spend budget =
1427-
decr budget;
1428-
if !budget < 0 then raise Give_up
1429-
14301426
(* Must follow the same edges as [occur_rec] in expression mode. Finishing
14311427
proves [occur_rec] would neither meet [ty0] nor find a [Tconstr] cycle,
1432-
its only ways to raise, and it expands types only after raising. *)
1433-
let occur_reaches ty0 ty =
1434-
let budget = ref walk_budget in
1435-
(* One closure for the whole walk, not a partial application per node. *)
1436-
let rec walk ty =
1437-
match ty.desc with
1438-
| Tlink ty -> walk ty
1439-
| _ -> (
1440-
if ty == ty0 then raise Occur;
1441-
spend budget;
1442-
match ty.desc with
1443-
| Tobject _ | Tvariant _ -> ()
1444-
| _ -> iter_type_expr walk ty)
1445-
in
1446-
walk ty
1428+
its only ways to raise, and it expands types only after raising. The
1429+
edges are those of [iter_type_expr], spelled out so that the walk
1430+
allocates nothing: it returns the budget left instead of keeping it in a
1431+
ref that a closure captures. *)
1432+
let rec occur_reaches_budget ty0 budget ty =
1433+
match ty.desc with
1434+
| Tlink ty -> occur_reaches_budget ty0 budget ty
1435+
| desc -> (
1436+
if ty == ty0 then raise Occur;
1437+
let budget = budget - 1 in
1438+
if budget < 0 then raise Give_up;
1439+
match desc with
1440+
| Tobject _ | Tvariant _ | Tvar _ | Tnil | Tunivar _ -> budget
1441+
| Tarrow (params, ret) ->
1442+
occur_reaches_budget ty0 (occur_reaches_params ty0 budget params) ret
1443+
| Ttuple l | Tconstr (_, l, _) | Tpackage (_, _, l) ->
1444+
occur_reaches_list ty0 budget l
1445+
| Tfield {typ; rest} ->
1446+
occur_reaches_budget ty0 (occur_reaches_budget ty0 budget typ) rest
1447+
| Tlink ty | Tsubst ty -> occur_reaches_budget ty0 budget ty
1448+
| Tpoly (ty, tyl) ->
1449+
occur_reaches_list ty0 (occur_reaches_budget ty0 budget ty) tyl)
1450+
1451+
and occur_reaches_list ty0 budget = function
1452+
| [] -> budget
1453+
| ty :: l -> occur_reaches_list ty0 (occur_reaches_budget ty0 budget ty) l
1454+
1455+
and occur_reaches_params ty0 budget = function
1456+
| [] -> budget
1457+
| {typ} :: l ->
1458+
occur_reaches_params ty0 (occur_reaches_budget ty0 budget typ) l
1459+
1460+
let occur_reaches ty0 ty = ignore (occur_reaches_budget ty0 walk_budget ty)
14471461

14481462
let occur_cannot_fail ty0 ty =
14491463
match occur_reaches ty0 ty with
@@ -1703,19 +1717,41 @@ let mkvariant fields closed =
17031717
(* Must follow the same edges as [deep_occur_marked]. Answers only "absent":
17041718
it gives up on meeting [t0] (where the marked walk stops early) and on a
17051719
node an enclosing walk marked (which the marked walk skips but then
1706-
unmarks in [unmark_type]), so it never leaves marks that differ. *)
1720+
unmarks in [unmark_type]), so it never leaves marks that differ. Like
1721+
[occur_reaches_budget], it spells out the edges of [iter_type_expr] so
1722+
that it allocates nothing, except at a [Tvariant]. *)
1723+
let rec deep_occur_budget t0 budget ty =
1724+
let ty = repr ty in
1725+
if ty.level < lowest_level then raise Give_up;
1726+
if ty == t0 then raise Occur;
1727+
let budget = budget - 1 in
1728+
if budget < 0 then raise Give_up;
1729+
match ty.desc with
1730+
| Tvar _ | Tnil | Tunivar _ -> budget
1731+
| Tarrow (params, ret) ->
1732+
deep_occur_budget t0 (deep_occur_params t0 budget params) ret
1733+
| Ttuple l | Tconstr (_, l, _) | Tpackage (_, _, l) ->
1734+
deep_occur_list t0 budget l
1735+
| Tobject ty | Tlink ty | Tsubst ty -> deep_occur_budget t0 budget ty
1736+
| Tfield {typ; rest} ->
1737+
deep_occur_budget t0 (deep_occur_budget t0 budget typ) rest
1738+
| Tpoly (ty, tyl) -> deep_occur_list t0 (deep_occur_budget t0 budget ty) tyl
1739+
| Tvariant _ ->
1740+
let budget = ref budget in
1741+
iter_type_expr (fun ty -> budget := deep_occur_budget t0 !budget ty) ty;
1742+
!budget
1743+
1744+
and deep_occur_list t0 budget = function
1745+
| [] -> budget
1746+
| ty :: l -> deep_occur_list t0 (deep_occur_budget t0 budget ty) l
1747+
1748+
and deep_occur_params t0 budget = function
1749+
| [] -> budget
1750+
| {typ} :: l -> deep_occur_params t0 (deep_occur_budget t0 budget typ) l
1751+
17071752
let deep_occur_fast_absent t0 ty =
1708-
let budget = ref walk_budget in
1709-
let rec walk ty =
1710-
let ty = repr ty in
1711-
if ty.level < lowest_level then raise Give_up
1712-
else (
1713-
if ty == t0 then raise Occur;
1714-
spend budget;
1715-
iter_type_expr walk ty)
1716-
in
1717-
match walk ty with
1718-
| () -> true
1753+
match deep_occur_budget t0 walk_budget ty with
1754+
| _ -> true
17191755
| exception (Occur | Give_up) -> false
17201756

17211757
(* [deep_occur_fast_absent] must follow the same edges. *)

0 commit comments

Comments
 (0)