-
Notifications
You must be signed in to change notification settings - Fork 47.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
compiler: More precise escape analysis #29782
Conversation
Our current escape analysis currently works by determing values which are returned, finding the scope those values are produced in, and then walking the dependencies of those scopes. Notably, when we traverse into scopes, we _force_ their values to be memoized — even in cases where we can prove it isn't necessary. The idea of this PR is to change escape analysis to be purely based on the flow of values. So rather than assume all scope deps need to be force-memoized, we look at how those values are used. Note: the main motivation for this PR is the follow-up, which allows us to infer dependencies for pruned scopes. Without this change, inferring deps for pruned scopes causes the escape analysis pass to consider values as escaping which shouldn't be. [ghstack-poisoned]
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
Comparing: eabb681...d9b212d Critical size changesIncludes critical production bundles, as well as any change greater than 2%:
Significant size changesIncludes any change greater than 0.2%: Expand to show
|
const y = makeObject(a); | ||
let t1; | ||
if ($[2] !== a) { | ||
t1 = makeObject(a); | ||
$[2] = a; | ||
$[3] = t1; | ||
} else { | ||
t1 = $[3]; | ||
} | ||
const y = t1; | ||
let t2; | ||
if ($[4] !== x || $[5] !== y.method || $[6] !== b) { | ||
t2 = x[y.method](b); | ||
$[4] = x; | ||
$[5] = y.method; | ||
$[6] = b; | ||
$[7] = t2; | ||
if ($[2] !== x || $[3] !== y.method || $[4] !== b) { | ||
t1 = x[y.method](b); | ||
$[2] = x; | ||
$[3] = y.method; | ||
$[4] = b; | ||
$[5] = t1; | ||
} else { | ||
t2 = $[7]; | ||
t1 = $[5]; | ||
} | ||
const z = t2; | ||
const z = t1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a perfect example of how this PR makes escape analysis more precise. The dependency is on y.method
, which we only use where a primitive is expected (in x[y.method]
). We don't have to memoize the makeObject(a)
call if we're only going to extract a primitive from it.
const cond = identity(false); | ||
let t1; | ||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) { | ||
t1 = identity(false); | ||
$[0] = t1; | ||
} else { | ||
t1 = $[0]; | ||
} | ||
const cond = t1; | ||
let t2; | ||
if ($[1] !== arr) { | ||
t2 = cond ? { val: CONST_TRUE } : mutate(arr); | ||
$[1] = arr; | ||
$[2] = t2; | ||
if ($[0] !== arr) { | ||
t1 = cond ? { val: CONST_TRUE } : mutate(arr); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This output is invalid — note that arr
gets mutated in the memo block, but declared outside of it — but that's because this is a bug that is fixed in the new HIR-based reactive scopes mode (which is on by default, this fixture opts out with @enableReactiveScopesInHIR:false
). The version of this fixture using HIR-based reactive scopes doesn't change as a result of this PR, and both the arr = shallowCopy(input)
and identity(false)
are correctly inside the memo block for arr
Stack from ghstack (oldest at bottom):
Our current escape analysis currently works by determing values which are returned, finding the scope those values are produced in, and then walking the dependencies of those scopes. Notably, when we traverse into scopes, we force their values to be memoized — even in cases where we can prove it isn't necessary.
The idea of this PR is to change escape analysis to be purely based on the flow of values. So rather than assume all scope deps need to be force-memoized, we look at how those values are used.
Note: the main motivation for this PR is the follow-up, which allows us to infer dependencies for pruned scopes. Without this change, inferring deps for pruned scopes causes the escape analysis pass to consider values as escaping which shouldn't be.
EDIT: we have to think more about cases like this:
where the output on this PR is:
Note that the
const x = makeObject()
isn't memoized, because we correctly determine that x doesn't escape. Here,makeObject()
should be pure such that it's result never changes, and therefore it should be fine to recalculatex
on every render and know thatif (x)
can't have changed in the memo block. But forcingmakeObject()
to evaluate once (by memoizing it w/o deps) guarantees thatx
can't change.Of course, if you're violating purity and
makeObject()
can change, than both our current compilation and the changed version from this PR would get errors, ignoring the updating value. But one advantage of our current compilation is that we force all usage of x to get the same, memoized value which feels slightly better due to consistency.