Skip to content

TypeInference: Only re-infer functions whose inputs changed - #2276

Open
Pajn wants to merge 1 commit into
meta:static_hfrom
Pajn:type-inference-sweep-changed-functions
Open

Pajn wants to merge 1 commit into
meta:static_hfrom
Pajn:type-inference-sweep-changed-functions

Conversation

@Pajn

@Pajn Pajn commented Oct 29, 2026

Copy link
Copy Markdown

Summary

TypeInference repeats rounds over a group of functions until no type changes, and every round inferred every instruction in the group. On large inputs most of that work repeats results that cannot have changed. This PR makes each round infer a function's instructions only when the function might infer differently. Parameters, return types and variables are still inferred for the whole group every round, as before.

A function's instructions are inferred again in the next round when:

  • its instructions changed type in this round,
  • its parameters changed type (in which case it is inferred in the same round),
  • it uses a function whose return type changed, or
  • it uses a variable whose type changed.

This gives the same types because, during the instruction sweep, a function's instructions only read types from the function itself, its parameters, the return types of known call targets (inferCallInst) and the variables it loads (inferLoadFrameInst). Types cross between functions only in the parameter, return type, variable and reset steps, and those steps now mark the functions they affect.

do {
infer params for every function           // marks F for this round's sweep
-  infer instructions of every function
+  infer instructions of each marked function // marks F for the next round
infer return types for every function     // marks users of F for the next round
infer types of every variable             // marks users of V for the next round
reset NoTypes once if nothing changed     // marks what it resets
} while (anything changed)

Why it matters for React Native

A Metro bundle forms a single group: the bundle's top-level function creates every module factory, so partitionFunctionsAndVars returns one partition. On a production React Native app bundle (8.2 MB of JavaScript, about 46k functions and 1.5M instructions), each TypeInference run took 12 to 15 rounds over all 1.5M instructions, though after the third round at most a few thousand instructions changed type per round. TypeInference runs four times in an -O compile and was 42% to 47% of the compile.

Instructions whose type changed in each round of the first TypeInference run on that bundle:

Round 1 2 3 4 5–9 10 11 (after reset) 12–15
Changed 367,368 373,047 30,122 6,271 3,022 0 23,170 735

Test Plan

Same output. I compiled the same app's iOS and Android production bundles with hermesc -emit-binary -O, using builds of static_h at 72b0009 with and without this change. The bytecode is byte-identical for both bundles, on macOS (Apple M5, Apple clang) and on x86-64 Linux (GCC 13).

Regression tests. check-hermes passes with the change: 4,334 expected passes, 6 expected failures, 190 unsupported, no unexpected results.

Compile time. Best of 3 runs of hermesc -emit-binary -O on the same bundles. TypeInference is the sum of its four entries in -ftime-report.

Machine and bundle Whole compile TypeInference
Apple M5, iOS bundle 7.85s → 6.27s 3.73s → 1.70s
x86-64 Linux CI runner, GCC 13, iOS bundle 26.18s → 20.46s 12.14s → 5.85s
x86-64 Linux CI runner, GCC 13, Android bundle 26.01s → 20.25s 12.28s → 6.77s

To reproduce on any large Metro bundle:

cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
ninja -C build hermesc
build/bin/hermesc -emit-binary -O -ftime-report -out out.hbc bundle.js 2>&1 | grep TypeInference

Summary:
TypeInference repeats rounds over a group of functions until no type
changes, and every round inferred every instruction in the group. Each
round now infers the instructions of a function only when the function
might infer differently: when its instructions changed in the previous
round, its parameters changed in this round, or it uses a function
whose return type or a variable whose type changed. Parameters, return
types and variables are still inferred for the whole group every round.

This gives the same types because, during the instruction sweep, a
function's instructions only read types from the function itself, its
parameters, the return types of known call targets and the variables
it loads. Those cross between functions only in the parameter, return
type, variable and reset steps, which mark the functions they affect.

A Metro bundle forms a single group, since the bundle's top-level
function creates every module factory. On a React Native app's
production bundle (8.2 MB, 46k functions, 1.5M instructions), each
TypeInference run took 12 to 15 rounds over all 1.5M instructions,
though after the third round at most a few thousand changed type.

`hermesc -O` on that bundle, best of 3:

  machine                             whole compile    TypeInference
  Apple M5                            7.85s -> 6.27s   3.73s -> 1.70s
  x86-64 Linux CI, GCC 13, iOS        26.18s -> 20.46s 12.14s -> 5.85s
  x86-64 Linux CI, GCC 13, Android    26.01s -> 20.25s 12.28s -> 6.77s

The bytecode is byte-identical before and after for both the iOS and
Android bundles, and check-hermes passes.
@meta-cla meta-cla Bot added the CLA Signed Do not delete this pull request or issue due to inactivity. label Oct 29, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed Do not delete this pull request or issue due to inactivity.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant