Skip to content
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

Measure class and functional approach performance differences #3

Open
dgp1130 opened this issue Sep 27, 2023 · 2 comments
Open

Measure class and functional approach performance differences #3

dgp1130 opened this issue Sep 27, 2023 · 2 comments

Comments

@dgp1130
Copy link
Owner

dgp1130 commented Sep 27, 2023

I suspect the functional approach is actually slower than the class approach because it may be less monomorphic. Every instance of a component in the class approach uses the same prototype containing all the exposed methods, meaning they have a shared definition which can be optimized together.

However in the functional approach, each component instance gets its own methods directly assigned by the hydrate function, meaning each component has different methods. I suspect this is less optimizable because it is harder for the JIT optimizer to apply speculative execution for multiple instances of a curried function definition compared to method on a class prototype.

I happened to recently read some articles like this which talk about how to inspect the optimization state in v8. This isn't the only performance difference between the two approaches, but it would be good to verify whether this difference actually exists and what its real-world impact might be. Could make a good blog post for sure.

Ideally I'd love to do the same for Gecko and WebKit, to avoid being Chrome-only here, though I'll need to do more research.

It is always possible this point is somewhat moot as web component methods may not be hot paths for most websites. Web components are tightly coupled to the UI being presented, meaning every method call is likely tied to a rerender, which is bounded at the refresh rate of the display. This isn't a hard limit or anything like that, but my point is that if a function is being called 1000x a second, it probably isn't a web component method, but instead some part of the underlying data model which will instead be rerendered on the next animation frame. Would be really cool if we could exercise some test workloads of major web component-based applications and see if any methods actually get optimized like this today.

Another factor is the use case of HydroActive, which is prerendered with limited interactivity. If you are building a complex SPA with web components, HydroActive isn't going to be a good fit, so optimizing for that use case probably isn't the best path forward. Lots of nuances to consider here.

@dgp1130
Copy link
Owner Author

dgp1130 commented Dec 3, 2023

So I took a quick stab at this today and built a local copy of d8. However I wasn't able to reproduce the results of that blog post, even for the simple add cases. This is before actually looking at a hypothetical component call. In the blog post %DebugPrint shows feedback from function invocations, but in my case I don't see that. I get:

 - feedback vector: No feedback vector, but we have a closure feedback cell array
0x2d9000001fd5: [ClosureFeedbackCellArray] in ReadOnlySpace

Also --trace-deopt doesn't print anything, even when blatantly changing argument types after function optimization.

I'm not sure what I'm doing wrong here and unfortunately there isn't a lot of documentation to go off of.

@dgp1130
Copy link
Owner Author

dgp1130 commented Dec 7, 2023

Matthias Liedtke graciously pointed out that --no-lazy-feedback-allocation (or a large number of add calls) is now necessary to trigger function optimization.

https://hachyderm.io/@mliedtke/111534982983887317

With that I'm able to reproduce the blog's results and test out my own code. I put the test code and results in this gist: https://gist.github.com/dgp1130/b86b0909401b6632792ee71e7e6038df

The key takeaways are in the README there, reproducing below for convenience:

  1. Optimizations do carry over between class component instances. When one instance
    is optimized, all instances are optimized.
  2. Optimizations do not carry over between components in the functional design. If
    comp.add is optimized, comp2.add will not automatically gain that optimization.
  3. The feedback vector is shared between functional components. functional.output
    lists 0x34bd000dacc9 as the feedback vector for both component add functions.
    • comp.sub does not contain a feedback vector and serves as a negative test of
      feedback vector behavior.
  4. The second component in functional.js seems to take significantly more effort to
    optimize. It requires around 500 invocations before V8 will optimize it (the first
    component does fine with 50). No idea why this takes significantly more calls.
    • Does sharing the feedback vector provide any value if it takes this much effort to
      trigger an optimization pass anyways? The browser could just as easily build a new
      feedback vector in that time.

This demonstrates that class components do optimize more effectively. How significant that is remains to be seen. I think we may need to do a micro-benchmark to understand exactly how much performance is being left on the table here.

That said, I am still thinking such deoptimizations are unlikely to cause real-world issues given HydroActive's actual use case. I suspect it will be rare for web component authors to commonly create a component with many instances holding methods which are called many times but disparately between instances. After all, if one particular instance receives most of the method calls, it will be optimized all the same. Performance is only comparatively worse to classes if there is a lot of work going through class methods and that work is not centralized to a small number of component instances. It's worth keeping in mind that HydroActive doesn't really have a render function which gets called on any state change.

The number of components on the page is limited by the number of DOM nodes (assuming every component is connected, which isn't always true but usually true). Lighthouse recommends < ~1,500 DOM nodes on the page at any time for performance.

There are also opt-outs if this becomes a real issue for specific, performance sensitive components. A HydroActive component could have a method which provides a "handle" object which all subsequent messages flow through. That object would not be subject to the deoptimizations.

Also since HydroActive is designed to be interoperable with other web components, there should be decent support for "ejecting" highly performance sensitive components and using a different framework or hand writing them entirely. That component would still be interoperable with every other HydroActive component on the page.

I don't particularly like pointing users away from HydroActive for this specific case, and I would rather provide a "pit of success" so users don't have to discover performance pitfalls like this. But also I'm skeptical about the real impact of this performance hit, especially as compared to the benefits of smoother variable scoping and nullish typing. A benchmark here would be very useful to understand how big the performance difference actually is.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant