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

perf(engine-core): avoid Array#splice in mutation tracking #4129

Merged
merged 3 commits into from Apr 11, 2024

Conversation

nolanlawson
Copy link
Contributor

@nolanlawson nolanlawson commented Apr 10, 2024

Details

Instead of doing an expensive Array#splice in this perf-sensitive mutation tracking code, we can

  1. swap the target item with the last item
  2. call Array#pop instead

I couldn't find an existing benchmark to repro the improvement in Chrome, so I wrote a new one. This new test builds a tree structure with recursive components. The reason it repros the improvement is that it has lots of component instances that are tracking mutations on a single getter (which is probably something that happens in the real world fairly frequently).

The improvement is 20-24%:

Screenshot 2024-04-10 at 12 36 05 PM

Here it is in a manual test to show the improvement to the reset() function:

Screenshot 2024-04-10 at 12 50 30 PM

FWIW I got this idea from Svelte here.

(I assume that the reason the splice is slow is because it has to create an additional array under the hood, plus it has to shift a bunch of array items around.)

Does this pull request introduce a breaking change?

  • 😮‍💨 No, it does not introduce a breaking change.

Does this pull request introduce an observable change?

  • 🤞 No, it does not introduce an observable change.

This is not an observable change because all the pending rehydration callbacks are queued up in a microtask, and then we sort the queue before running them:

const vms = rehydrateQueue.sort((a: VM, b: VM): number => a.idx - b.idx);

GUS work item

@nolanlawson nolanlawson requested a review from a team as a code owner April 10, 2024 19:52
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this benchmark for the heck of it. There is no improvement from this PR, since this PR targets reset() which happens during component removal.

// (Avoiding splice here is a perf optimization, and the order doesn't matter.)
const index = ArrayIndexOf.call(set, this);
set[index] = set[setLength - 1];
ArrayPop.call(set);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a performance difference between set.pop() and set.length = setLength - 1? We use the former here, but the latter in the if block.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good catch. We can actually refactor the code to simplify it, if we're always just poping or setting set.length = length - 1 in every case.

76f07c4

I did a quick perf test, and there's basically no difference between set.length = length - 1:

Screenshot 2024-04-10 at 2 00 59 PM

... and pop():

Screenshot 2024-04-10 at 2 06 21 PM

Although maybe pop() is slightly faster? Either way I would prefer pop() since it's more idiomatic.

I'm also planning to run this through our full benchmark suite tonight, just in case there is some weird edge case I missed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh, so it looks like pop() is faster than setting length. In a minimal comparison, the benchmark reports that pop() is 1-4% faster:

Screenshot 2024-04-10 at 3 14 06 PM


before(() => {
treeElement = createElement('benchmark-tree', { is: Tree });
// Not really 3k, but close enough: 5^5 = 3,125
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making the file 55 characters longer to save 2 characters in the filename! 🚀

nolanlawson and others added 2 commits April 10, 2024 13:51
@nolanlawson
Copy link
Contributor Author

OK, running the full benchmark suite of this PR against master, it looks like it's basically pure improvement. (There's one 1ms regression that I'm not too concerned about.)

Screenshot 2024-04-11 at 8 14 28 AM

@nolanlawson nolanlawson merged commit 355cec4 into master Apr 11, 2024
9 checks passed
@nolanlawson nolanlawson deleted the nolan/array-pop branch April 11, 2024 17:28
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

Successfully merging this pull request may close these issues.

None yet

2 participants