From e7f0c04b17527c967c92e3c2712a029cbde7176f Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Sat, 10 Oct 2026 09:58:16 +0000 Subject: [PATCH 1/4] Stop measuring lambda size once the inlining limit is reached Callers only compare the size with small thresholds, but it was computed for the whole body at every call site of a known function. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Christoph Knittel --- compiler/core/lam_analysis.ml | 123 ++++++++++++++++++------------ compiler/core/lam_analysis.mli | 4 +- compiler/core/lam_pass_exits.ml | 4 +- compiler/core/lam_stats_export.ml | 5 +- 4 files changed, 83 insertions(+), 53 deletions(-) diff --git a/compiler/core/lam_analysis.ml b/compiler/core/lam_analysis.ml index 1fa89078b0f..c48edd4b86b 100644 --- a/compiler/core/lam_analysis.ml +++ b/compiler/core/lam_analysis.ml @@ -141,34 +141,57 @@ let rec no_side_effects (lam : Lambda.t) : bool = | Lapply _ -> false (* we need purity analysis .. *) -(* - Estimate the size of lambda for better inlining - threshold is 1000 - so that we -*) -exception Too_big_to_inline +(* Size of a lambda that is too big to inline, e.g. one containing a loop *) +let too_big = 1000 -let really_big () = raise_notrace Too_big_to_inline +let rec size_constant (x : Lambda.structured_constant) = + match x with + | Const_int _ | Const_assertfalse | Const_constructor _ | Const_char _ + | Const_float _ | Const_bigint _ | Const_polyvar _ | Const_js_null + | Const_js_undefined _ | Const_module_alias | Const_js_true | Const_js_false + -> + 1 + | Const_string _ -> 1 + | Const_some s -> size_constant s + | Const_block (_, str) -> + Ext_list.fold_left str 0 (fun acc x -> acc + size_constant x) -(* let big_lambda = 1000 *) +exception Limit_reached -let rec size (lam : Lambda.t) = - try +(* + Estimate the size of a lambda for inlining. Callers only compare it with + small thresholds, so stop counting once [limit] is reached: the result is + the size if it is below [limit], and [limit] otherwise. +*) +let size_upto ~limit (lam : Lambda.t) = + assert (limit <= too_big); + let total = ref 0 in + let add n = + total := !total + n; + if !total >= limit then raise_notrace Limit_reached + in + let rec size (lam : Lambda.t) = match lam with - | Lvar _ -> 1 - | Lconst c -> size_constant c - | Llet (_, _, l1, l2) -> 1 + size l1 + size l2 - | Lletrec _ -> really_big () + | Lvar _ -> add 1 + | Lconst c -> add (size_constant c) + | Llet (_, _, l1, l2) -> + add 1; + size l1; + size l2 + | Lletrec _ -> add too_big | Lprim { primitive = Pfield (_, Fld_module _); args = [(Lglobal_module _ | Lvar _)]; _; } -> - 1 + add 1 | Lprim {primitive = Praise | Pis_not_none; args = [l]; _} -> size l - | Lglobal_module _ -> 1 - | Lprim {primitive = Praw_js_code _} -> really_big () - | Lprim {args = ll; _} -> size_lams 1 ll + | Lglobal_module _ -> add 1 + | Lprim {primitive = Praw_js_code _} -> add too_big + | Lprim {args = ll; _} -> + add 1; + List.iter size ll (* complicated 1. inline this function 2. ... @@ -177,39 +200,37 @@ let rec size (lam : Lambda.t) = {var $$let=Make(funarg); return [0, $$let[5],... $$let[16]]} *) - | Lapply {ap_func; ap_args; _} -> size_lams (size ap_func) ap_args - (* | Lfunction(_, params, l) -> really_big () *) + | Lapply {ap_func; ap_args; _} -> + size ap_func; + List.iter size ap_args | Lfunction {body} -> size body - | Lswitch _ -> really_big () - | Lstringswitch (_, _, _) -> really_big () + | Lswitch _ -> add too_big + | Lstringswitch (_, _, _) -> add too_big | Lstaticraise (_i, ls) -> - Ext_list.fold_left ls 1 (fun acc x -> size x + acc) - | Lstaticcatch _ -> really_big () - | Ltrywith _ -> really_big () - | Lifthenelse (l1, l2, l3) -> 1 + size l1 + size l2 + size l3 - | Lsequence (l1, l2) -> size l1 + size l2 - | Lbreak | Lcontinue -> 1 - | Lwhile _ -> really_big () - | Lfor _ -> really_big () - | Lfor_of _ | Lfor_await_of _ -> really_big () - | Lassign (_, v) -> 1 + size v + add 1; + List.iter size ls + | Lstaticcatch _ -> add too_big + | Ltrywith _ -> add too_big + | Lifthenelse (l1, l2, l3) -> + add 1; + size l1; + size l2; + size l3 + | Lsequence (l1, l2) -> + size l1; + size l2 + | Lbreak | Lcontinue -> add 1 + | Lwhile _ -> add too_big + | Lfor _ -> add too_big + | Lfor_of _ | Lfor_await_of _ -> add too_big + | Lassign (_, v) -> + add 1; + size v (* This is side effectful, be careful *) - with Too_big_to_inline -> 1000 - -and size_constant x = - match x with - | Const_int _ | Const_assertfalse | Const_constructor _ | Const_char _ - | Const_float _ | Const_bigint _ | Const_polyvar _ | Const_js_null - | Const_js_undefined _ | Const_module_alias | Const_js_true | Const_js_false - -> - 1 - | Const_string _ -> 1 - | Const_some s -> size_constant s - | Const_block (_, str) -> - Ext_list.fold_left str 0 (fun acc x -> acc + size_constant x) - -and size_lams acc (lams : Lambda.t list) = - Ext_list.fold_left lams acc (fun acc l -> acc + size l) + in + match size lam with + | () -> !total + | exception Limit_reached -> limit let args_all_const (args : Lambda.t list) = Ext_list.for_all args (fun x -> @@ -241,13 +262,15 @@ let destruct_pattern (body : Lambda.t) params args = | Lswitch (Lvar v, switch) -> ( match aux v params args with | Some (Lambda.Lconst _ as lam) -> - size (Lambda.switch lam switch) < small_inline_size + size_upto ~limit:small_inline_size (Lambda.switch lam switch) + < small_inline_size | Some _ | None -> false) | Lifthenelse (Lvar v, then_, else_) -> ( (* -FIXME *) match aux v params args with | Some (Lconst _ as lam) -> - size (Lambda.if_ lam then_ else_) < small_inline_size + size_upto ~limit:small_inline_size (Lambda.if_ lam then_ else_) + < small_inline_size | Some _ | None -> false) | _ -> false @@ -263,7 +286,7 @@ let ok_to_inline_fun_when_app (m : Lambda.lfunction) (args : Lambda.t list) = | Default_inline -> ( match m with | {body; params} -> - let s = size body in + let s = size_upto ~limit:10 body in s < small_inline_size || destruct_pattern body params args || (args_all_const args && s < 10 && no_side_effects body)) diff --git a/compiler/core/lam_analysis.mli b/compiler/core/lam_analysis.mli index e42f736f14d..ba40a3b1b37 100644 --- a/compiler/core/lam_analysis.mli +++ b/compiler/core/lam_analysis.mli @@ -27,7 +27,9 @@ val no_side_effects : Lambda.t -> bool (** No side effect, but it might depend on data store *) -val size : Lambda.t -> int +val size_upto : limit:int -> Lambda.t -> int +(** The size of a lambda for inlining decisions if it is below [limit], + [limit] otherwise. [limit] must be at most 1000. *) val lfunction_can_be_inlined : Lambda.lfunction -> bool diff --git a/compiler/core/lam_pass_exits.ml b/compiler/core/lam_pass_exits.ml index 4113b0cf1ad..418e5c13f1b 100644 --- a/compiler/core/lam_pass_exits.ml +++ b/compiler/core/lam_pass_exits.ml @@ -173,7 +173,9 @@ let subst_helper (subst : subst_tbl) (query : int -> int) (lam : Lambda.t) : let ok_to_inline = i >= 0 && no_bounded_variables l2' && - let lam_size = Lam_analysis.size l2' in + let lam_size = + Lam_analysis.size_upto ~limit:Lam_analysis.exit_inline_size l2' + in (i_occur <= 2 && lam_size < Lam_analysis.exit_inline_size) || lam_size < 5 in diff --git a/compiler/core/lam_stats_export.ml b/compiler/core/lam_stats_export.ml index 54fea1e5de7..6544529faca 100644 --- a/compiler/core/lam_stats_export.ml +++ b/compiler/core/lam_stats_export.ml @@ -77,7 +77,10 @@ let values_of_export (meta : Lam_stats.t) (export_map : Lambda.t Map_ident.t) : optlam else None | _ -> - let lam_size = Lam_analysis.size lambda in + let lam_size = + Lam_analysis.size_upto ~limit:Lam_analysis.small_inline_size + lambda + in (* TODO: 1. global need re-assocate when do the beta reduction 2. [lambda_exports] is not precise From 5121bcd9ec3eb39b4f0701b39c754f0d72947b29 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Sat, 10 Oct 2026 10:16:06 +0000 Subject: [PATCH 2/4] Add CHANGELOG entry Co-Authored-By: Claude Opus 5.5 Signed-off-by: Christoph Knittel --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index adaa2ef2340..3a5ad9f2ea2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ - Compile dict spreads to object spread syntax instead of `Object.assign`. https://github.com/rescript-lang/rescript/pull/8747 - Make compilation slightly faster and stop rewatch from copying files twice for modules with an interface. https://github.com/rescript-lang/rescript/pull/8769 +- Speed up inlining decisions for large functions. https://github.com/rescript-lang/rescript/pull/8771 - Represent explicit expression braces as `Pexp_braces` in parsetree v1 and format `else` branches consistently with `if` branches. https://github.com/rescript-lang/rescript/pull/8678 - Omit redundant braces around multi-statement switch case bodies when formatting. https://github.com/rescript-lang/rescript/pull/8677 - Avoid running `rescript-schema-ppx` and `sury-ppx` on source files without an `@schema` annotation. https://github.com/rescript-lang/rescript/pull/8662 From fdf9ea2ec46b7505a041f7686f721b8f958de7c7 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Sat, 10 Oct 2026 10:41:19 +0000 Subject: [PATCH 3/4] Add tests for the lambda size cutoff Unit tests for size_upto at the inlining limits, and a fixture with calls on both sides of the thresholds (its JS is the same as before the change). Co-Authored-By: Claude Opus 5.5 Signed-off-by: Christoph Knittel --- tests/ounit_tests/ounit_lam_size_tests.ml | 44 +++++ tests/ounit_tests/ounit_tests_main.ml | 1 + .../tests/src/inline_size_threshold_test.mjs | 161 ++++++++++++++++++ .../tests/src/inline_size_threshold_test.res | 65 +++++++ 4 files changed, 271 insertions(+) create mode 100644 tests/ounit_tests/ounit_lam_size_tests.ml create mode 100644 tests/tests/src/inline_size_threshold_test.mjs create mode 100644 tests/tests/src/inline_size_threshold_test.res diff --git a/tests/ounit_tests/ounit_lam_size_tests.ml b/tests/ounit_tests/ounit_lam_size_tests.ml new file mode 100644 index 00000000000..bb136f02323 --- /dev/null +++ b/tests/ounit_tests/ounit_lam_size_tests.ml @@ -0,0 +1,44 @@ +open OUnit + +let ( =~ ) = assert_equal ~printer:string_of_int + +let x = Ident.create "x" + +(* [Lstaticraise] counts 1 plus its arguments and a variable counts 1, so this + lambda has exactly size [n + 1]. *) +let raise_of_vars n = Lambda.staticraise 0 (List.init n (fun _ -> Lambda.var x)) + +(* A loop is too big to inline, whatever its size. *) +let loop = Lambda.while_ (Lambda.var x) (Lambda.var x) + +let size_upto = Lam_analysis.size_upto + +let suites = + __FILE__ + >::: [ + ( "returns the size below the limit and the limit otherwise" + >:: fun _ -> + (* the limits used by the inlining heuristics *) + List.iter + (fun limit -> + for n = 0 to limit + 3 do + size_upto ~limit (raise_of_vars n) =~ min (n + 1) limit + done) + [Lam_analysis.small_inline_size; Lam_analysis.exit_inline_size; 10] + ); + ( "adds up nested lambdas" >:: fun _ -> + (* 1 + (1 + 2) + (1 + 1) = 6 *) + let lam = Lambda.let_ Strict x (raise_of_vars 2) (raise_of_vars 1) in + size_upto ~limit:10 lam =~ 6; + size_upto ~limit:7 lam =~ 6; + size_upto ~limit:6 lam =~ 6; + size_upto ~limit:5 lam =~ 5 ); + ( "counts a construct that is too big to inline as the limit" + >:: fun _ -> + size_upto ~limit:10 loop =~ 10; + size_upto ~limit:5 + (Lambda.staticraise 0 [Lambda.var x; loop; Lambda.var x]) + =~ 5; + (* the old unbounded size was 1000 for these *) + size_upto ~limit:1000 loop =~ 1000 ); + ] diff --git a/tests/ounit_tests/ounit_tests_main.ml b/tests/ounit_tests/ounit_tests_main.ml index 7475226fad8..084313bd55d 100644 --- a/tests/ounit_tests/ounit_tests_main.ml +++ b/tests/ounit_tests/ounit_tests_main.ml @@ -22,6 +22,7 @@ let suites = Ounit_lambda_traverse_tests.suites; Ounit_deep_flatten_tests.suites; Ounit_exits_tests.suites; + Ounit_lam_size_tests.suites; Ounit_sroa_tests.suites; Ounit_ast_mapper0_tests.suites; Ounit_constructor_arguments_tests.suites; diff --git a/tests/tests/src/inline_size_threshold_test.mjs b/tests/tests/src/inline_size_threshold_test.mjs new file mode 100644 index 00000000000..a9898d88328 --- /dev/null +++ b/tests/tests/src/inline_size_threshold_test.mjs @@ -0,0 +1,161 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as Mocha from "mocha"; +import * as Test_utils from "./test_utils.mjs"; + +function g1(x, y) { + return x * y | 0; +} + +function g2(x, y) { + return (x * y | 0) + x | 0; +} + +function g3(x, y) { + return ((x * y | 0) + x | 0) + x | 0; +} + +function g4(x, y) { + return (((x * y | 0) + x | 0) + x | 0) + x | 0; +} + +function g5(x, y) { + return ((((x * y | 0) + x | 0) + x | 0) + x | 0) + x | 0; +} + +function g6(x, y) { + return (((((x * y | 0) + x | 0) + x | 0) + x | 0) + x | 0) + x | 0; +} + +function g7(x, y) { + return ((((((x * y | 0) + x | 0) + x | 0) + x | 0) + x | 0) + x | 0) + x | 0; +} + +function g8(x, y) { + return (((((((x * y | 0) + x | 0) + x | 0) + x | 0) + x | 0) + x | 0) + x | 0) + x | 0; +} + +function g9(x, y) { + return ((((((((x * y | 0) + x | 0) + x | 0) + x | 0) + x | 0) + x | 0) + x | 0) + x | 0) + x | 0; +} + +function g10(x, y) { + return (((((((((x * y | 0) + x | 0) + x | 0) + x | 0) + x | 0) + x | 0) + x | 0) + x | 0) + x | 0) + x | 0; +} + +function g11(x, y) { + return ((((((((((x * y | 0) + x | 0) + x | 0) + x | 0) + x | 0) + x | 0) + x | 0) + x | 0) + x | 0) + x | 0) + x | 0; +} + +function g12(x, y) { + return (((((((((((x * y | 0) + x | 0) + x | 0) + x | 0) + x | 0) + x | 0) + x | 0) + x | 0) + x | 0) + x | 0) + x | 0) + x | 0; +} + +function pick(b, x) { + if (b) { + return x + 1 | 0; + } else { + return (x << 1); + } +} + +function withVariables(a, b) { + return [ + a * b | 0, + g2(a, b), + g3(a, b), + g4(a, b), + g5(a, b), + g6(a, b), + g7(a, b), + g8(a, b), + g9(a, b), + g10(a, b), + g11(a, b), + g12(a, b) + ]; +} + +function withConstants() { + return [ + 6, + 8, + 10, + 12, + g5(2, 3), + g6(2, 3), + g7(2, 3), + g8(2, 3), + g9(2, 3), + g10(2, 3), + g11(2, 3), + g12(2, 3) + ]; +} + +function picked(a) { + return [ + a + 1 | 0, + (a << 1) + ]; +} + +Mocha.describe("File \"inline_size_threshold_test.res\", line 59, characters 9-16", () => { + Mocha.test("calls around the inlining thresholds", () => { + Test_utils.eq("File \"inline_size_threshold_test.res\", line 61, characters 7-14", withVariables(2, 3), [ + 6, + 8, + 10, + 12, + 14, + 16, + 18, + 20, + 22, + 24, + 26, + 28 + ]); + Test_utils.eq("File \"inline_size_threshold_test.res\", line 62, characters 7-14", withConstants(), [ + 6, + 8, + 10, + 12, + 14, + 16, + 18, + 20, + 22, + 24, + 26, + 28 + ]); + Test_utils.eq("File \"inline_size_threshold_test.res\", line 63, characters 7-14", [ + 6, + 10 + ], [ + 6, + 10 + ]); + }); +}); + +export { + g1, + g2, + g3, + g4, + g5, + g6, + g7, + g8, + g9, + g10, + g11, + g12, + pick, + withVariables, + withConstants, + picked, +} +/* Not a pure module */ diff --git a/tests/tests/src/inline_size_threshold_test.res b/tests/tests/src/inline_size_threshold_test.res new file mode 100644 index 00000000000..160de5309cf --- /dev/null +++ b/tests/tests/src/inline_size_threshold_test.res @@ -0,0 +1,65 @@ +// Functions whose bodies grow in size around the inlining thresholds +// (Lam_analysis.small_inline_size, the limit for constant arguments and +// exit_inline_size): the generated JS shows which calls get inlined. +open Mocha +open Test_utils + +let g1 = (x, y) => x * y +let g2 = (x, y) => x * y + x +let g3 = (x, y) => x * y + x + x +let g4 = (x, y) => x * y + x + x + x +let g5 = (x, y) => x * y + x + x + x + x +let g6 = (x, y) => x * y + x + x + x + x + x +let g7 = (x, y) => x * y + x + x + x + x + x + x +let g8 = (x, y) => x * y + x + x + x + x + x + x + x +let g9 = (x, y) => x * y + x + x + x + x + x + x + x + x +let g10 = (x, y) => x * y + x + x + x + x + x + x + x + x + x +let g11 = (x, y) => x * y + x + x + x + x + x + x + x + x + x + x +let g12 = (x, y) => x * y + x + x + x + x + x + x + x + x + x + x + x + +let pick = (b, x) => + if b { + x + 1 + } else { + x * 2 + } + +let withVariables = (a, b) => [ + g1(a, b), + g2(a, b), + g3(a, b), + g4(a, b), + g5(a, b), + g6(a, b), + g7(a, b), + g8(a, b), + g9(a, b), + g10(a, b), + g11(a, b), + g12(a, b), +] + +let withConstants = () => [ + g1(2, 3), + g2(2, 3), + g3(2, 3), + g4(2, 3), + g5(2, 3), + g6(2, 3), + g7(2, 3), + g8(2, 3), + g9(2, 3), + g10(2, 3), + g11(2, 3), + g12(2, 3), +] + +let picked = a => (pick(true, a), pick(false, a)) + +describe(__LOC__, () => { + test("calls around the inlining thresholds", () => { + eq(__LOC__, withVariables(2, 3), [6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28]) + eq(__LOC__, withConstants(), [6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28]) + eq(__LOC__, picked(5), (6, 10)) + }) +}) From a08dc5c66403b0dbb76c8d4c7c5bdaeed6a9e3e9 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Sat, 10 Oct 2026 10:53:47 +0000 Subject: [PATCH 4/4] Stop at the size limit inside constants too Large constant lists or records were still walked completely before the limit was checked. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Christoph Knittel --- compiler/core/lam_analysis.ml | 26 +++++++++++------------ tests/ounit_tests/ounit_lam_size_tests.ml | 18 ++++++++++++++++ 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/compiler/core/lam_analysis.ml b/compiler/core/lam_analysis.ml index c48edd4b86b..272b62d60e8 100644 --- a/compiler/core/lam_analysis.ml +++ b/compiler/core/lam_analysis.ml @@ -144,18 +144,6 @@ let rec no_side_effects (lam : Lambda.t) : bool = (* Size of a lambda that is too big to inline, e.g. one containing a loop *) let too_big = 1000 -let rec size_constant (x : Lambda.structured_constant) = - match x with - | Const_int _ | Const_assertfalse | Const_constructor _ | Const_char _ - | Const_float _ | Const_bigint _ | Const_polyvar _ | Const_js_null - | Const_js_undefined _ | Const_module_alias | Const_js_true | Const_js_false - -> - 1 - | Const_string _ -> 1 - | Const_some s -> size_constant s - | Const_block (_, str) -> - Ext_list.fold_left str 0 (fun acc x -> acc + size_constant x) - exception Limit_reached (* @@ -170,10 +158,22 @@ let size_upto ~limit (lam : Lambda.t) = total := !total + n; if !total >= limit then raise_notrace Limit_reached in + (* Constants count their leaves, and stop at the limit too: a large constant + list or record should not be walked completely. *) + let rec constant (c : Lambda.structured_constant) = + match c with + | Const_int _ | Const_assertfalse | Const_constructor _ | Const_char _ + | Const_float _ | Const_bigint _ | Const_polyvar _ | Const_js_null + | Const_js_undefined _ | Const_module_alias | Const_js_true | Const_js_false + | Const_string _ -> + add 1 + | Const_some s -> constant s + | Const_block (_, str) -> List.iter constant str + in let rec size (lam : Lambda.t) = match lam with | Lvar _ -> add 1 - | Lconst c -> add (size_constant c) + | Lconst c -> constant c | Llet (_, _, l1, l2) -> add 1; size l1; diff --git a/tests/ounit_tests/ounit_lam_size_tests.ml b/tests/ounit_tests/ounit_lam_size_tests.ml index bb136f02323..34bd62023ed 100644 --- a/tests/ounit_tests/ounit_lam_size_tests.ml +++ b/tests/ounit_tests/ounit_lam_size_tests.ml @@ -13,6 +13,16 @@ let loop = Lambda.while_ (Lambda.var x) (Lambda.var x) let size_upto = Lam_analysis.size_upto +let int i = Lambda.const_int i + +(* A constant list of [n] elements, nested [n] levels deep *) +let constant_list n = + let rec go i acc = + if i < 0 then acc + else go (i - 1) (Lambda.Const_block (Blk_tuple, [int i; acc])) + in + go (n - 1) (int 0) + let suites = __FILE__ >::: [ @@ -41,4 +51,12 @@ let suites = =~ 5; (* the old unbounded size was 1000 for these *) size_upto ~limit:1000 loop =~ 1000 ); + ( "counts the leaves of constants" >:: fun _ -> + let tuple = Lambda.Const_block (Blk_tuple, [int 1; int 2; int 3]) in + size_upto ~limit:10 (Lambda.const tuple) =~ 3; + size_upto ~limit:10 (Lambda.const (Lambda.Const_some (int 1))) =~ 1; + size_upto ~limit:2 (Lambda.const tuple) =~ 2 ); + ( "stops inside large constants" >:: fun _ -> + (* would need a million nested calls without the cutoff *) + size_upto ~limit:5 (Lambda.const (constant_list 1_000_000)) =~ 5 ); ]