-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
Disable useDefineForClassFields
for ~10% memory and 10-16% performance improvement
#3884
Conversation
🦋 Changeset detectedLatest commit: 24b6123 The changes in this PR will be included in the next version bump. This PR includes changesets to release 5 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
useDefineForClassFields
for 10-20% memory and 12-16% performance improvementsuseDefineForClassFields
for 10-20% memory and 12-16% performance improvement
useDefineForClassFields
for 10-20% memory and 12-16% performance improvementuseDefineForClassFields
for ~10% memory and 12-16% performance improvement
useDefineForClassFields
for ~10% memory and 12-16% performance improvementuseDefineForClassFields
for ~5-10% memory and 10-16% performance improvement
useDefineForClassFields
for ~5-10% memory and 10-16% performance improvementuseDefineForClassFields
for ~10% memory and 10-16% performance improvement
Interesting! I think to get a proper read on the performance impact, we should actually update the perf suite to use ESM builds without downcompiled classes; generally speaking not declaring fields upfront in the constructor, will hit deoptimized paths of the engine as soon as you use such as a field, as the fields in the constructor are used by the JIT to generate so called inner classes to optimize member access (roughly: instead of a map look up, all members declared in the constructor can be looked up using constant offsets. So my suspicion would be that the above is maybe faster for down CJS possibly, but likely slower on envs where first class class are idiomatically supported. |
Thank you @mweststrate and nice catch - I think being exhaustive is preferable - I didn't think to test non-downcompiled ESM outputs. I migrated the tests to use non-downcompiled classes and ESM in taj-p#3. I generated the below results. I think that maps / shape classes (the term the JS engine uses for representing the "shape" of some object) extends to objects as well as classes, so, I'm not sure if it would affect classes significantly differently from class-like downcompiled objects. I'll confer and check my understanding with a colleague. ResultsProxies
I also ran a "proxy"-only performance test 100 times consecutively for the before and after using
Legacy
AnalysisPerformance seems better when disabling Please let me know if you'd like me to create a PR/include in this PR:
ExtraWow! Non-downcompiled classes are a bit faster than their downcompiled counterparts. Is there an opportunity here for us to target ES6 without class downcompilation? I can open a discussion if you'd like. Compilation ChangesWe can see how properties are not added on instantiation in the "After ObservableValue" screenshot. (I've shown the development outputs below, but did use the production output for testing). Before ObservableValueAfter ObservableValueExtra: ResultsThe below results are taken from using the downcompiled-class ESM outputs (i.e., our current ESM build outputs) in the perf suite: Proxy mode
Legacy mode
|
Thanks for the detailed follow up @taj-p! tagging @peterm-canva who is likely interested in the results here! Happy to create a beta build to make validation easier. |
The microbenchmarks from this repo don't really exercise the code in a way that would detect regressions due to deopts from shape changes. That's totally fine. What we'd need is larger macrobenchmarks where a whole application is running, to simulate a more realistic usage pattern. On canva I see that this change has a slightly (0.5% ish) positive effect on page load metrics. That only indicates how we use mobx though. There's plenty of other valid usage patterns, and I think it would be possible to construct a scenario with a regression. That said, the memory and initialization-time improvements of this change could end up cancelling those out in a lot of cases too. It's hard to say what the overall effect would be without more macrobenchmarks. If there are open-source projects that use mobx and have macrobenchmarks, it'd be great long-term to set those up so we can measure changes like this more readily. It's true this approach of not adding fields to an object until they are used does go against typical performance advice for JS. But it also gets nice memory wins for very heavily used objects. It can also be undone fairly easily if there are issues reported, or undone partially by setting an explicit |
Ok happy to give this is shot, will likely unroll if noteworthy regressions are reported. Let's release #3901 first however in a separate version, to better be able to pinpoint any potential regression |
@mweststrate - do you mind holding off on merge for about a week? There are 2 actions I'd like to complete first to ensure we don't introduce regression:
Sorry for the delay, I've been caught up in other work. |
Sure, no problem!
…On Thu, 18 Jul 2024, 01:01 Taj Pereira, ***@***.***> wrote:
@mweststrate <https://github.com/mweststrate> - do you mind holding off
on merge for about a week? There are 2 actions I'd like to complete first
to ensure we don't introduce regression:
1. Measure page load memory and latency for a variety of document
types (the above solid analysis by Peter measures the opening of an empty
document, I'd like to also test on medium and large documents)
2. Conduct an internal testing party with the team on the changes to
exercise our application through various scenarios
Sorry for the delay, I've been caught up in other work.
—
Reply to this email directly, view it on GitHub
<#3884 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAN4NBHXWBWAUQMHH7WZW73ZM3ZVPAVCNFSM6AAAAABITRWZWGVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDEMZUGU3TQMBWGM>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
@taj-p what where your further findings? |
Sorry for the late response @mweststrate ! We deployed this to a test branch in Canva - although we saw memory savings, we also saw page load regression by 1 to 2%. I think it isn't worth further investigation for now, so I'll close the PR. |
Intent
By disabling
useDefineForClassFields
in Mobx build artifacts, we can:(Consumer code still requires enabling
useDefineForClassFields
to use decorators - see "why won't this break decorators" heading).When we
useDefineForClassFields: true
, we output code that looks like the following:Which results in instances that look like:
Of
ObservableValues
' 16 members, 5 members are initially undefined, but, are still allocated as keys on the instance. In many codebases (and ours), most of our mobx class instances have at least a couple unused members (such as undefined listeners/interceptors) whose memory we pay for hundreds of thousands of times.When we disable
useDefineForClassFields
, those unused members aren't allocated in the class instance until they're used because we don't need to define them ahead of time in the constructor:The reason this occurs is because when
useDefineForClassFields
is enabled, we actually define all potentially used fields ahead of time (as exampled below) - this costs us both memory (in unused allocations) and performance (in unused initialised properties).Won't this break decorators?
So long as consumers enable
useDefineForClassFields
, there is no loss of functionality. Skipping thesedefineProperty
calls in every Mobx constructor has no bearing on whether consumer classes usedefineProperty
. So, the proposed changes don't affect the decorator bookkeeping that is performed on consumer code (whereuseDefineForClassFields
is enabled).To emphasize: disabling
useDefineForClassFields
only prevents that behaviour for our build outputs - consumers still need to enableuseDefineForClassFields
to use decorators (and for Mobx decorator enrichment/bookkeeping/checking to work).Although we depart from how our consumer code may build, disabling
useDefineForClassFields
gains consumers appreciable performance and memory improvements which I think may be more important.Note: the test suite has been amended to enable
useDefineForClassFields
in order to correctly test decorators. When disabled, only consumer based generated JavaScript breaks. I'm happy to manually transpile class constructors in the test suite to simulateuseDefineForClassFields
.For reference, I am able to use this branch without issue in my large codebase that uses thousands of decorators.
Memory Savings: ~10-20%
Each undefined reference consumes 28 bytes according to this memory heap snapshot. But, to be honest, I don't know if I'm reading this right. In any case, there's some cost to having an undefined value and some cost attributed to the name of the property.
To test the memory improvements, I created the below test program which allocates a million boxes and runs them all in a single autorun. We were able to achieve 10% reduction in memory, 23% reduction in
ObservableValue
shallow size, and 11% reduction inObservableValue
retained size.Test program
Results
Screenshots
Note:
t
in these screenshots refers toObservableValue
(I used a production build for testing).Before: Total 803 MB, ObservableValue: 104 MB
After: 719 MB, ComputedValue: 80 MB
Performance Improvements: ~12-15%
Running the performance suite a few times actually shows significant improvement when disabling
useDefineForClassFields
: somewhere between 12 - 16%. By not declaring these fields upfront, these classes will go through a few more state transitions; but, it seems that setting those properties ahead of time has a larger cost on performance.I think the performance improvement is due to the lack of a bunch of
Object. defineProperty
running in each class constructor.Proxy mode tests: ~12% improvement
Legacy mode tests: ~15% improvement
Extra: Bundle Size falls by 1.7%
Bundle size, as you can expect with the fewer
Object.defineProperty
, falls by 1.7% (uncompressed prod cjs 52953 B VS 52020 B)Code change checklist
/docs
. For new functionality, at leastAPI.md
should be updatedyarn mobx test:performance
)