Skip to content

Commit 27cb0bf

Browse files
committed
Remember type path lookups within a compilation
Why: Typechecking a 552 module React app calls Env.find_type_full 1.76M times, and normalize_path 666k times from expand_abbrev. Most calls look up the same few paths against the same environment: `option` alone is 528k of them, followed by `string`, `array`, `Jsx.element`. Each lookup walks the open layers of env.types, and expanding a type that isn't an abbreviation also normalizes its path, which raises and catches Not_found to return the path unchanged. A direct-mapped table now remembers successful lookups and unchanged normalizations. An entry is only reused while the environment tables the lookup read are physically the same (env.types for a Pident, env.components for a Pdot, env.modules and env.components for a normalization), and only within the request that stored it. Changed normalizations are computed again, so paths in types are allocated as before. Paths with a functor application aren't cached, the memo is skipped while loading .cmi files is disabled, and it is emptied when a request ends and when a loaded module is replaced. On the corpus 94% of lookups and 96% of normalizations hit, and the output is byte identical to master. Review: Can an entry be stale? The tables an entry depends on are immutable, so the same physical table gives the same answer. New unit tests bind the same path differently in two environments within one request (recursive type placeholders, recursive module approximations, an alias) and fail if any of these checks is removed. Signed-off-by: Jono Prest <jjprest@gmail.com>
1 parent 227b697 commit 27cb0bf

12 files changed

Lines changed: 907 additions & 13 deletions

‎changes/type-path-memo.polish.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Build: remember type path lookups within a compilation, which avoids looking up the same types such as `option` and `string` hundreds of thousands of times per build.

‎compiler/ml/ctype.ml‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1201,11 +1201,11 @@ let expand_abbrev_gen kind find_type_expansion env ty =
12011201
ty'
12021202
| None -> (
12031203
match find_type_expansion path env with
1204-
| exception Not_found ->
1204+
| exception Not_found -> (
12051205
(* another way to expand is to normalize the path itself *)
1206-
let path' = Env.normalize_path None env path in
1207-
if Path.same path path' then raise Cannot_expand
1208-
else newty2 level (Tconstr (path', args, abbrev))
1206+
match Env.normalize_type_path env path with
1207+
| None -> raise Cannot_expand
1208+
| Some path' -> newty2 level (Tconstr (path', args, abbrev)))
12091209
| params, body, lv ->
12101210
(* prerr_endline
12111211
("add a "^string_of_kind kind^" expansion for "^Path.name path);*)

‎compiler/ml/env.ml‎

Lines changed: 199 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -563,11 +563,109 @@ type pers_struct = {
563563
ps_filename: string;
564564
}
565565

566+
(* Request-local memo of type path lookups ([find_type_full] on regular
567+
paths) and of [normalize_path] calls that leave a type path unchanged.
568+
569+
The typechecker resolves the same type paths over and over (expanding
570+
[option], [Js.Null.t], ... in [expand_head], [update_level], [occur_univar],
571+
...), each time walking the [open] layers of [env.types] or the components
572+
of a persistent module. Both lookups only read immutable environment tables
573+
([env.types] for [Pident] type paths, [env.components] for [Pdot] ones,
574+
[env.modules] and [env.components] for normalization) plus request-wide
575+
state that only grows: persistent structures (replacing one starts a new
576+
memo epoch, see [add_persistent_structure]) and module components, which
577+
are forced at most once. An entry is therefore
578+
reused only when the tables it read are physically the same, and only
579+
successful results are stored. A repeated call has no side effect the memo
580+
would skip: the first, uncached call already loaded the [.cmi]s and forced
581+
the lazy components it needed. Paths that contain a functor application are
582+
never cached ([find_module_descr] applies the functor against [env]), and
583+
the memo is bypassed while [.cmi] loading is disabled ([without_cmis] may
584+
backtrack components). *)
585+
module Type_path_memo = struct
586+
let size = 4096 (* a power of two *)
587+
588+
let string_hash s =
589+
let n = String.length s in
590+
if n = 0 then 0
591+
else
592+
(n * 31)
593+
+ (Char.code (String.unsafe_get s 0) * 7)
594+
+ Char.code (String.unsafe_get s (n - 1))
595+
596+
exception Not_cacheable
597+
598+
(* Consistent with [equal]: it ignores [pos] and ident flags, which no lookup
599+
reads. *)
600+
let rec hash (path : Path.t) =
601+
match path with
602+
| Pident id -> (id.Ident.stamp * 65599) + string_hash id.name
603+
| Pdot (p, s, _pos) -> (hash p * 31) + string_hash s
604+
| Papply _ -> raise_notrace Not_cacheable
605+
606+
let slot path = hash path land (size - 1)
607+
608+
(* Paths that every lookup in this file resolves identically: identifiers are
609+
looked up by name and stamp, and components by name. *)
610+
let rec equal (p1 : Path.t) (p2 : Path.t) =
611+
p1 == p2
612+
||
613+
match (p1, p2) with
614+
| Pident id1, Pident id2 ->
615+
id1.Ident.stamp = id2.Ident.stamp && String.equal id1.name id2.name
616+
| Pdot (p1, s1, _), Pdot (p2, s2, _) -> String.equal s1 s2 && equal p1 p2
617+
| _ -> false
618+
619+
type ('types, 'modules, 'components, 'result) entry = {
620+
epoch: unit ref;
621+
path: Path.t;
622+
types: 'types;
623+
modules: 'modules;
624+
components: 'components;
625+
result: 'result;
626+
}
627+
628+
type ('types, 'modules, 'components, 'result) t =
629+
('types, 'modules, 'components, 'result) entry option array
630+
631+
let create () : _ t = Array.make size None
632+
let clear (t : _ t) = Array.fill t 0 size None
633+
end
634+
635+
(* The tables are allocated once per domain rather than per request: they are
636+
large, and requests are small. An entry is valid only in the epoch that
637+
stored it. Each request starts a fresh epoch (and a nested request restores
638+
the outer one when it returns), so entries never cross requests; clearing
639+
starts a new epoch too. *)
640+
let type_path_memo_key =
641+
Domain.DLS.new_key (fun () ->
642+
(Type_path_memo.create (), Type_path_memo.create ()))
643+
644+
let type_path_memo_epoch_key = Request_local.new_key (fun () -> ref (ref ()))
645+
let type_path_memo_epoch () = !(Domain.DLS.get type_path_memo_epoch_key)
646+
647+
let clear_type_path_memo_tables () =
648+
let found, unchanged = Domain.DLS.get type_path_memo_key in
649+
Type_path_memo.clear found;
650+
Type_path_memo.clear unchanged
651+
652+
let clear_type_path_memo () =
653+
Domain.DLS.get type_path_memo_epoch_key := ref ();
654+
clear_type_path_memo_tables ()
655+
566656
let persistent_structures_key =
567657
Request_local.new_key (fun () ->
568658
(Hashtbl.create 17 : (string, pers_struct option) Hashtbl.t))
569659
let persistent_structures () = Domain.DLS.get persistent_structures_key
570660

661+
(* [Hashtbl.add] shadows an earlier binding for [name], which the type path
662+
memo may have read (directly, or as a missing module), so replacing one
663+
starts a new memo epoch. *)
664+
let add_persistent_structure name ps =
665+
let tbl = persistent_structures () in
666+
if Hashtbl.mem tbl name then clear_type_path_memo ();
667+
Hashtbl.add tbl name ps
668+
571669
(* Consistency between persistent structures *)
572670

573671
let crc_units_key = Request_local.new_key Consistbl.create
@@ -604,7 +702,7 @@ let check_consistency ps =
604702

605703
let save_pers_struct crc ps =
606704
let modname = ps.ps_name in
607-
Hashtbl.add (persistent_structures ()) modname (Some ps);
705+
add_persistent_structure modname (Some ps);
608706
Consistbl.set (crc_units ()) modname crc ps.ps_filename;
609707
add_import modname
610708

@@ -649,7 +747,7 @@ let acknowledge_pers_struct check modname {Persistent_signature.filename; cmi} =
649747
if ps.ps_name <> modname then
650748
error (Illegal_renaming (modname, ps.ps_name, filename));
651749
if check then check_consistency ps;
652-
Hashtbl.add (persistent_structures ()) modname (Some ps);
750+
add_persistent_structure modname (Some ps);
653751
ps
654752

655753
let read_pers_struct check modname filename =
@@ -670,7 +768,7 @@ let find_pers_struct check name =
670768
match !Persistent_signature.load ~unit_name:name with
671769
| Some ps -> ps
672770
| None ->
673-
Hashtbl.add (persistent_structures ()) name None;
771+
add_persistent_structure name None;
674772
raise Not_found
675773
in
676774
add_import name;
@@ -715,6 +813,7 @@ let check_pers_struct name =
715813

716814
let reset_cache () =
717815
current_unit () := "";
816+
clear_type_path_memo ();
718817
Hashtbl.clear (persistent_structures ());
719818
clear_imports ();
720819
Hashtbl.clear (value_declarations ());
@@ -731,13 +830,16 @@ let reset_cache_toplevel () =
731830
(persistent_structures ()) []
732831
in
733832
List.iter (Hashtbl.remove (persistent_structures ())) l;
833+
clear_type_path_memo ();
734834
Hashtbl.clear (value_declarations ());
735835
Hashtbl.clear (type_declarations ());
736836
Hashtbl.clear (module_declarations ());
737837
Hashtbl.clear (used_constructors ());
738838
Hashtbl.clear (prefixed_sg ())
739839

740-
let set_unit_name name = current_unit () := name
840+
let set_unit_name name =
841+
clear_type_path_memo ();
842+
current_unit () := name
741843

742844
let get_unit_name () = !(current_unit ())
743845

@@ -784,11 +886,44 @@ let type_of_cstr path = function
784886
(d, ([], List.map snd (Datarepr.labels_of_type path d)))
785887
| _ -> assert false
786888

889+
(* [find_type_full p env] for a regular type path, memoized as described at
890+
[Type_path_memo]. *)
891+
let find_regular_type_full p env =
892+
match !(can_load_cmis ()) with
893+
| Cannot_load_cmis _ -> find_type_full p env
894+
| Can_load_cmis -> (
895+
match Type_path_memo.slot p with
896+
| exception Type_path_memo.Not_cacheable -> find_type_full p env
897+
| i -> (
898+
let memo, _ = Domain.DLS.get type_path_memo_key in
899+
let epoch = type_path_memo_epoch () in
900+
match Array.unsafe_get memo i with
901+
| Some e
902+
when e.epoch == epoch
903+
&& (match p with
904+
| Pident _ -> e.types == env.types
905+
| Pdot _ | Papply _ -> e.components == env.components)
906+
&& Type_path_memo.equal e.path p ->
907+
e.result
908+
| _ ->
909+
let result = find_type_full p env in
910+
Array.unsafe_set memo i
911+
(Some
912+
{
913+
epoch;
914+
path = p;
915+
types = env.types;
916+
modules = ();
917+
components = env.components;
918+
result;
919+
});
920+
result))
921+
787922
let find_type_full path env =
788923
match Path.constructor_typath path with
789924
| Regular p -> (
790925
try (Path_map.find p env.local_constraints, ([], []))
791-
with Not_found -> find_type_full p env)
926+
with Not_found -> find_regular_type_full p env)
792927
| Cstr (ty_path, s) ->
793928
let _, (cstrs, _) =
794929
try find_type_full ty_path env with Not_found -> assert false
@@ -895,6 +1030,46 @@ let normalize_path oloc env path =
8951030
| Some loc ->
8961031
raise (Error (Missing_module (loc, path, normalize_path true env path))))
8971032

1033+
(* [Some (normalize_path None env path)] when normalization changes [path]
1034+
([Path.same] is false), [None] when it leaves it unchanged. The unchanged
1035+
outcome is memoized as described at [Type_path_memo]; a changed path is
1036+
always computed afresh, so callers get a new path value as before. *)
1037+
let normalize_type_path env path =
1038+
let normalize () =
1039+
let path' = normalize_path None env path in
1040+
if Path.same path path' then None else Some path'
1041+
in
1042+
match !(can_load_cmis ()) with
1043+
| Cannot_load_cmis _ -> normalize ()
1044+
| Can_load_cmis -> (
1045+
match Type_path_memo.slot path with
1046+
| exception Type_path_memo.Not_cacheable -> normalize ()
1047+
| i -> (
1048+
let _, memo = Domain.DLS.get type_path_memo_key in
1049+
let epoch = type_path_memo_epoch () in
1050+
match Array.unsafe_get memo i with
1051+
| Some e
1052+
when e.epoch == epoch && e.modules == env.modules
1053+
&& e.components == env.components
1054+
&& Type_path_memo.equal e.path path ->
1055+
None
1056+
| _ ->
1057+
let result = normalize () in
1058+
(match result with
1059+
| None ->
1060+
Array.unsafe_set memo i
1061+
(Some
1062+
{
1063+
epoch;
1064+
path;
1065+
types = ();
1066+
modules = env.modules;
1067+
components = env.components;
1068+
result = ();
1069+
})
1070+
| Some _ -> ());
1071+
result))
1072+
8981073
let normalize_path_prefix oloc env path =
8991074
match path with
9001075
| Pdot (p, s, pos) -> Pdot (normalize_path oloc env p, s, pos)
@@ -1992,7 +2167,25 @@ let reset_initial_for_request () =
19922167
let with_fresh_initial action =
19932168
let previous = initial_safe_string () in
19942169
Fun.protect action ~finally:(fun () ->
1995-
Domain.DLS.set initial_safe_string_key previous)
2170+
Domain.DLS.set initial_safe_string_key previous;
2171+
(* The per-domain memo tables would otherwise keep the finished
2172+
request's environments alive until their slots are overwritten. Their
2173+
entries are already invalid outside the request's epoch. *)
2174+
clear_type_path_memo_tables ())
2175+
2176+
module For_test = struct
2177+
let type_path_memo_entries () =
2178+
let found, unchanged = Domain.DLS.get type_path_memo_key in
2179+
let count t =
2180+
Array.fold_left
2181+
(fun n e ->
2182+
match e with
2183+
| None -> n
2184+
| Some _ -> n + 1)
2185+
0 t
2186+
in
2187+
count found + count unchanged
2188+
end
19962189

19972190
let last_env_key = Request_local.new_key (fun () -> ref empty)
19982191
let last_env () = Domain.DLS.get last_env_key

‎compiler/ml/env.mli‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ val empty : t
3838
val initial_safe_string : unit -> t
3939
val reset_initial_for_request : unit -> unit
4040
val with_fresh_initial : (unit -> 'a) -> 'a
41+
(* Runs a request with its own initial environment; on return, also empties
42+
this domain's type path memo. *)
4143

4244
type type_descriptions = constructor_description list * label_description list
4345

@@ -77,6 +79,10 @@ val normalize_path : Location.t option -> t -> Path.t -> Path.t
7779
If the option is None, allow returning dangling paths.
7880
Otherwise raise a Missing_module error, and may add forgotten
7981
head as required global. *)
82+
val normalize_type_path : t -> Path.t -> Path.t option
83+
(* [normalize_type_path env path] is [Some (normalize_path None env path)] when
84+
that differs from [path] according to [Path.same], and [None] otherwise. *)
85+
8086
val normalize_path_prefix : Location.t option -> t -> Path.t -> Path.t
8187
(* Only normalize the prefix part of the path *)
8288

@@ -299,3 +305,8 @@ module Persistent_signature : sig
299305
the .cmi file in the load path. This function can be overridden to load
300306
it from memory, for instance to build a self-contained toplevel. *)
301307
end
308+
309+
module For_test : sig
310+
val type_path_memo_entries : unit -> int
311+
(** The type path memo entries held on this domain. *)
312+
end

‎compiler/ml/typecore.ml‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -814,9 +814,10 @@ let rec expand_path env p =
814814
match repr ty with
815815
| {desc = Tconstr (p, _, _)} -> expand_path env p
816816
| _ -> p (* PR#6394: recursive module may introduce incoherent manifest *))
817-
| _ ->
818-
let p' = Env.normalize_path None env p in
819-
if Path.same p p' then p else expand_path env p'
817+
| _ -> (
818+
match Env.normalize_type_path env p with
819+
| None -> p
820+
| Some p' -> expand_path env p')
820821

821822
let compare_type_path env tpath1 tpath2 =
822823
Path.same (expand_path env tpath1) (expand_path env tpath2)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
2+
Warning number 44
3+
/.../fixtures/warning_34_unused_type_shadowed.res:14:3-12
4+
5+
12 ┆ }
6+
13 ┆
7+
14 ┆ open Inner
8+
15 ┆ let w: t = w
9+
16 ┆ let v = (Some(String.length(w)): option<int>)
10+
11+
this open statement shadows the type identifier t (which is later used)
12+
13+
14+
Warning number 34
15+
/.../fixtures/warning_34_unused_type_shadowed.res:5:3-22
16+
17+
3 │ let w: string
18+
4 │ } = {
19+
5 │ type t = option<int>
20+
6 │ type unusedAlias = option<int>
21+
7 │
22+
23+
unused type t.
24+
25+
26+
Warning number 34
27+
/.../fixtures/warning_34_unused_type_shadowed.res:6:3-32
28+
29+
4 │ } = {
30+
5 │ type t = option<int>
31+
6 │ type unusedAlias = option<int>
32+
7 │
33+
8 │ module Inner = {
34+
35+
unused type unusedAlias.
36+
37+
38+
Warning number 34
39+
/.../fixtures/warning_34_unused_type_shadowed.res:10:5-24
40+
41+
8 ┆ module Inner = {
42+
9 ┆ type t = string
43+
10 ┆ type unusedInner = t
44+
11 ┆ let w: t = "w"
45+
12 ┆ }
46+
47+
unused type unusedInner.

0 commit comments

Comments
 (0)