Skip to content

Commit 4eabf11

Browse files
committed
Copy a dependency's signature only when it is read
Why: Env keeps each loaded compilation unit's signature as a lazy copy of the .cmi signature with its binders renamed (Subst.signature). normalize_path forced that copy just to learn that a compilation unit is not a module alias, which it never is; normalize_path is reached for every value reference such as Belt.Array.map, for abstract dependency types in Ctype.try_expand_once, and from Includemod and Printtyp. These copies were about 4-6 % of a clean build's CPU time on real projects (ui: 0.62 s of 13.3 s with one domain). normalize_path now only looks the unit up (keeping the .cmi load, import and CRC checks) and reserves the stamps and type ids the copy would take, so every later identifier and type node gets the same stamp and id as before. The copy is made with exactly those stamps and ids when the signature is first read (find_module, scrape_alias, fold_modules, read_signature); on a 552-module app 2,921 copies are reserved and 10 are ever made. How many stamps and ids a copy takes is learnt from the first copy of the same .cmi contents, keyed by the CRC create_cmi wrote, and kept for the process. If a reserved copy ever takes a different number, it is thrown away and made again with fresh stamps and ids, so a wrong count can change ids but never makes two nodes share one. Review: Are the artifacts the same? Yes: stamps and type ids are the same by construction. JS, .cmi, .cmj, .ast and .cmt are byte-identical to master on the 5,398-artifact corpus (default and one domain), and a unit test checks that deferred copies of every runtime and Belt interface marshal to the same bytes as eager ones, with the same stamps and ids created around them. Review: Can the copy differ when it is made later? Only if the .cmi graph changed in between; the type checker only works on copies of it, and closed object types' shared mutability cells, the one part it can change, do not change what is copied. Signed-off-by: Jono Prest <jjprest@gmail.com>
1 parent 32116b5 commit 4eabf11

15 files changed

Lines changed: 538 additions & 9 deletions
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Speed up type checking by copying a dependency's interface only when it is read, not when the compiler only checks whether it is a module alias.

‎compiler/ext/ident.ml‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,19 @@ let set_current_time t =
8282
let state = counter () in
8383
state.currentstamp <- max state.currentstamp t
8484

85+
let with_current_time t f =
86+
let state = counter () in
87+
let saved = state.currentstamp in
88+
state.currentstamp <- t;
89+
match f () with
90+
| result ->
91+
let final = state.currentstamp in
92+
state.currentstamp <- saved;
93+
(result, final)
94+
| exception exn ->
95+
state.currentstamp <- saved;
96+
raise exn
97+
8598
let reinit () =
8699
let state = counter () in
87100
if state.reinit_level < 0 then state.reinit_level <- state.currentstamp

‎compiler/ext/ident.mli‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ val is_predef_exn : t -> bool
5050
val binding_time : t -> int
5151
val current_time : unit -> int
5252
val set_current_time : int -> unit
53+
54+
val with_current_time : int -> (unit -> 'a) -> 'a * int
55+
56+
(* [with_current_time t f] runs [f] with the stamp counter set to [t], so the
57+
identifiers it creates get the stamps after [t], and returns [f]'s result
58+
with the counter value it ended at. The counter is then put back to its
59+
value before the call, also if [f] raises. Used to create identifiers whose
60+
stamps were reserved earlier. *)
5361
val reinit : unit -> unit
5462

5563
val capture_request_baseline : unit -> unit

‎compiler/ml/env.ml‎

Lines changed: 143 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -541,9 +541,113 @@ let current_unit () = Domain.DLS.get current_unit_key
541541

542542
(* Persistent structure descriptions *)
543543

544+
(* The signature of a persistent structure is a copy of the one in its .cmi
545+
file whose bound identifiers are renamed ([Subst.signature Subst.identity]).
546+
The copy is often only asked for to learn that a compilation unit is not a
547+
module alias ([normalize_path]), so it is not made then: the stamps and type
548+
ids it would have taken are reserved, so every later stamp and id is the
549+
same as if it had been made, and the copy is made with exactly those stamps
550+
and ids when the signature is first read.
551+
552+
The number of stamps and ids a copy takes is learnt from the first copy of
553+
the same .cmi contents (keyed by its CRC) and kept for the whole process.
554+
It only depends on the signature graph, which the type checker does not
555+
change before copying it: only copies of it are given to the rest of the
556+
type checker. If a reserved copy ever takes a different number of stamps or
557+
ids, it is thrown away and made again with fresh ones. *)
558+
559+
type pers_sig_state =
560+
| Sig_pending
561+
| Sig_reserved of {stamp_base: int; type_base: int; stamps: int; types: int}
562+
| Sig_copied of signature
563+
564+
type pers_sig = {
565+
pers_sig_raw: signature;
566+
pers_sig_key: Digest.t option;
567+
mutable pers_sig_state: pers_sig_state;
568+
}
569+
570+
let copy_counts : (Digest.t, int * int) Hashtbl.t = Hashtbl.create 64
571+
let copy_counts_lock = Mutex.create ()
572+
573+
let find_copy_counts key =
574+
Mutex.protect copy_counts_lock (fun () -> Hashtbl.find_opt copy_counts key)
575+
576+
let record_copy_counts key counts =
577+
Mutex.protect copy_counts_lock (fun () ->
578+
if Hashtbl.length copy_counts >= 65536 then Hashtbl.reset copy_counts;
579+
Hashtbl.replace copy_counts key counts)
580+
581+
let reserved_copies = Atomic.make 0
582+
let reserved_copy_fallbacks = Atomic.make 0
583+
584+
let pers_sig ?key raw =
585+
{pers_sig_raw = raw; pers_sig_key = key; pers_sig_state = Sig_pending}
586+
587+
let copy_pers_sig raw = Subst.signature Subst.identity raw
588+
589+
let force_pers_sig ps =
590+
match ps.pers_sig_state with
591+
| Sig_copied sg -> sg
592+
| Sig_pending ->
593+
let state = Compiler_request_state.current () in
594+
let stamp0 = Ident.current_time () and type0 = state.type_node_id in
595+
let sg = copy_pers_sig ps.pers_sig_raw in
596+
(match ps.pers_sig_key with
597+
| Some key ->
598+
record_copy_counts key
599+
(Ident.current_time () - stamp0, state.type_node_id - type0)
600+
| None -> ());
601+
ps.pers_sig_state <- Sig_copied sg;
602+
sg
603+
| Sig_reserved {stamp_base; type_base; stamps; types} ->
604+
let state = Compiler_request_state.current () in
605+
let saved_type_id = state.type_node_id in
606+
state.type_node_id <- type_base;
607+
let (sg, stamp_end), type_end =
608+
match
609+
Ident.with_current_time stamp_base (fun () ->
610+
copy_pers_sig ps.pers_sig_raw)
611+
with
612+
| result ->
613+
let type_end = state.type_node_id in
614+
state.type_node_id <- saved_type_id;
615+
(result, type_end)
616+
| exception exn ->
617+
state.type_node_id <- saved_type_id;
618+
raise exn
619+
in
620+
let sg =
621+
if stamp_end = stamp_base + stamps && type_end = type_base + types then sg
622+
else (
623+
(* Some of the stamps or ids may also belong to later identifiers and
624+
type nodes: copy again with fresh ones. *)
625+
Atomic.incr reserved_copy_fallbacks;
626+
copy_pers_sig ps.pers_sig_raw)
627+
in
628+
ps.pers_sig_state <- Sig_copied sg;
629+
sg
630+
631+
(* Take the stamps and type ids [force_pers_sig] would take now, without
632+
copying yet. *)
633+
let reserve_pers_sig ps =
634+
match ps.pers_sig_state with
635+
| Sig_copied _ | Sig_reserved _ -> ()
636+
| Sig_pending -> (
637+
match Option.bind ps.pers_sig_key find_copy_counts with
638+
| None -> ignore (force_pers_sig ps)
639+
| Some (stamps, types) ->
640+
let state = Compiler_request_state.current () in
641+
let stamp_base = Ident.current_time () in
642+
let type_base = state.type_node_id in
643+
Atomic.incr reserved_copies;
644+
Ident.set_current_time (stamp_base + stamps);
645+
state.type_node_id <- type_base + types;
646+
ps.pers_sig_state <- Sig_reserved {stamp_base; type_base; stamps; types})
647+
544648
type pers_struct = {
545649
ps_name: string;
546-
ps_sig: signature Lazy.t;
650+
ps_sig: pers_sig;
547651
ps_comps: module_components;
548652
ps_crcs: (string * Digest.t option) list;
549653
ps_filename: string;
@@ -607,6 +711,13 @@ module Persistent_signature = struct
607711
| exception Not_found -> None)
608712
end
609713

714+
(* The CRC create_cmi wrote for the contents of the .cmi (its name and
715+
signature). *)
716+
let own_crc (cmi : Cmi_format.cmi_infos) =
717+
match cmi.cmi_crcs with
718+
| (name, Some crc) :: _ when name = cmi.cmi_name -> Some crc
719+
| _ -> None
720+
610721
let acknowledge_pers_struct check modname {Persistent_signature.filename; cmi} =
611722
let name = cmi.cmi_name in
612723
let sign = cmi.cmi_sign in
@@ -626,7 +737,7 @@ let acknowledge_pers_struct check modname {Persistent_signature.filename; cmi} =
626737
let ps =
627738
{
628739
ps_name = name;
629-
ps_sig = lazy (Subst.signature Subst.identity sign);
740+
ps_sig = pers_sig ?key:(own_crc cmi) sign;
630741
ps_comps = comps;
631742
ps_crcs = crcs;
632743
ps_filename = filename;
@@ -822,7 +933,7 @@ let find_module ~alias path env =
822933
with Not_found ->
823934
if Ident.persistent id && not (Ident.name id = !(current_unit ())) then
824935
let ps = find_pers_struct (Ident.name id) in
825-
md (Mty_signature (Lazy.force ps.ps_sig))
936+
md (Mty_signature (force_pers_sig ps.ps_sig))
826937
else raise Not_found)
827938
| Pdot (p, s, _pos) -> (
828939
match get_components (find_module_descr p env) with
@@ -851,6 +962,26 @@ let find_module ~alias path env =
851962
mty))
852963
| Structure_comps _ -> raise Not_found)
853964

965+
(* The path a module is an alias of, as [find_module ~alias:true] gives it. A
966+
compilation unit is never an alias, so its signature is not copied. *)
967+
let module_alias_target path env =
968+
let alias_target = function
969+
| {md_type = Mty_alias (_, path1)} -> Some path1
970+
| _ -> None
971+
in
972+
match path with
973+
| Pident id -> (
974+
try
975+
let data = Id_tbl.find_same id env.modules in
976+
alias_target (Env_lazy.force subst_modtype_maker data)
977+
with Not_found ->
978+
if Ident.persistent id && not (Ident.name id = !(current_unit ())) then (
979+
let ps = find_pers_struct (Ident.name id) in
980+
reserve_pers_sig ps.ps_sig;
981+
None)
982+
else raise Not_found)
983+
| Pdot _ | Papply _ -> alias_target (find_module ~alias:true path env)
984+
854985
let rec normalize_path lax env path =
855986
let path =
856987
match path with
@@ -860,9 +991,9 @@ let rec normalize_path lax env path =
860991
| _ -> path
861992
in
862993
try
863-
match find_module ~alias:true path env with
864-
| {md_type = Mty_alias (_, path1)} -> normalize_path lax env path1
865-
| _ -> path
994+
match module_alias_target path env with
995+
| Some path1 -> normalize_path lax env path1
996+
| None -> path
866997
with
867998
| Not_found
868999
when lax
@@ -1912,9 +2043,12 @@ let open_signature ?(used_slot = ref false) ?(loc = Location.none) ovf root env
19122043

19132044
(* Read a signature from a file *)
19142045

2046+
let deferred_signature_copy_stats () =
2047+
(Atomic.get reserved_copies, Atomic.get reserved_copy_fallbacks)
2048+
19152049
let read_signature modname filename =
19162050
let ps = read_pers_struct modname filename in
1917-
Lazy.force ps.ps_sig
2051+
force_pers_sig ps.ps_sig
19182052

19192053
(* Return the CRC of the interface of the given compilation unit *)
19202054

@@ -1960,7 +2094,7 @@ let save_signature_with_imports ?check_exists ~deprecated sg modname filename
19602094
let ps =
19612095
{
19622096
ps_name = modname;
1963-
ps_sig = lazy (Subst.signature Subst.identity sg);
2097+
ps_sig = pers_sig sg;
19642098
ps_comps = comps;
19652099
ps_crcs = (cmi.cmi_name, Some crc) :: imports;
19662100
ps_filename = filename;
@@ -2029,7 +2163,7 @@ let fold_modules f lid env acc =
20292163
| Some ps ->
20302164
f name
20312165
(Pident (Ident.create_persistent name))
2032-
(md (Mty_signature (Lazy.force ps.ps_sig)))
2166+
(md (Mty_signature (force_pers_sig ps.ps_sig)))
20332167
acc)
20342168
(persistent_structures ()) acc
20352169
| Some l -> (

‎compiler/ml/env.mli‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,12 @@ val get_unit_name : unit -> string
166166
(* Read, save a signature to/from a file *)
167167

168168
val read_signature : string -> string -> signature
169+
170+
val deferred_signature_copy_stats : unit -> int * int
171+
(* For tests: how many copies of compilation units' signatures were deferred,
172+
and how many of those did not take the stamps and type ids reserved for
173+
them and were made again with fresh ones (expected: none), in the process
174+
so far. *)
169175
(* Arguments: module name, file name. Results: signature. *)
170176

171177
val save_signature :

0 commit comments

Comments
 (0)