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(collection): optimisations #10552

Merged
merged 14 commits into from
Nov 7, 2024

Conversation

Renegade334
Copy link
Contributor

@Renegade334 Renegade334 commented Oct 11, 2024

.first(), .firstKey()

The callback variant of Array.from() is slow in V8. Changing the method of array generation from:

Array.from({ length: amount }, () => ...)

to

const results = new Array(amount)
for (let index = 0; index < amount; index++) results[index] = ...

produces a ~90-95% reduction in execution time, when tested with all current LTS versions of Node.

Note that this is deliberately new Array(n) and not Array.from({ length: n }):

  • The first of these just creates an empty array with a length property of n, and is an O(1) operation.
  • The second of these is an O(n) operation: it creates an empty array of length n, then for all i in 0 <= i < n, it reads the property obj[i] from the object parameter (which in this case will always be undefined), and sets array[i] to that value. This is significantly slower for large arrays, and not what we want here.

Additionally, switches to just using iterable-to-array in the case where amount >= this.size, which is a fast operation for builtin iterables. This further halves the execution time when compared with the above method.

.last(), .lastKey()

Only carry out the memory-expensive [...this.<iterable>()] if needed.

Previously, passing amount <= 0 would still copy the entire collection into a temporary array, even though it's never referenced. This significantly speeds up execution in those cases.

Uses the new .at() or .keyAt() (as below) to fetch the last element in the collection in the case where amount === undefined. This is around 10-20% faster than the previous method of copying into a temporary array and selecting the last element.

.at(), .keyAt()

Manually iterate to the target index, instead of generating a full array copy of the collection to call Array.prototype.at().

The performance of the previous implementation was essentially linear on the size of the collection, and independent of the index being fetched, as the whole collection was copied into a temporary array regardless.

The performance of the new implementation is linear on the index being fetched, and results in a performance improvement of >90% when the target index is close to the start of the collection. In the worst case when the target index is close to the end of the collection, it still performs well (around 10-20% faster in Node v22, and around 50% faster in Node v18). It also avoids the memory cost of copying the collection into a temporary array.

This contains a small off-by-one fix which represents a technical breaking change. Previously, the implementations of .at() and .keyAt() were not compliant with the standard for Array.prototype.at() specifically when passing a negative non-integer index: the standard dictates that these are truncated (ie. rounded towards zero), whereas the previous implementation used Math.floor() (ie. rounded these away from zero).

.random(), .randomKey()

The previous implementation used repeated calls to Array.prototype.splice() to extract individual random elements from a temporary array copy of the collection. This is a very slow process for larger arrays, since each splice necessitates a memory move of all elements above the extracted element, so performing repeated splices for each random element becomes very inefficient.

The new implementation uses an in-place Durstenfeld shuffle, stopping after enough elements have been randomised, and then returns the shuffled elements using Array.prototype.slice(). This is about 40% faster for small collections, and >90% faster for large ones.

Additionally, skips copying the collection into a temporary array if amount === 0, and uses the new .at() or .keyAt() (as above) to fetch a single element in the case where amount === undefined.

.map()

Changes the method of array generation to pushing to a new array, instead of passing a callback to Array.from().

Same rationale as .first() above. This change produces a ~80% reduction in execution time when tested with all current LTS versions of Node.

.merge()

Adjusts the logic for checking hasInSelf and hasInOther. Previously, each of these boolean conditions was evaluated twice; this makes a small change to the control flow to deduplicate the checks.

.toSorted()

Removes the redundant closure wrapping compareFunction and just passes it directly, eliminating a needless call from the stack.

Status and versioning classification:

  • Code changes have been tested against the Discord API, or there are no code changes
  • I know how to update typings and have done so, or typings don't need updating

Copy link

vercel bot commented Oct 11, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

2 Skipped Deployments
Name Status Preview Comments Updated (UTC)
discord-js ⬜️ Ignored (Inspect) Visit Preview Nov 7, 2024 10:57am
discord-js-guide ⬜️ Ignored (Inspect) Visit Preview Nov 7, 2024 10:57am

Copy link

codecov bot commented Oct 11, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 38.16%. Comparing base (ed78e45) to head (c71e032).
Report is 8 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #10552      +/-   ##
==========================================
+ Coverage   38.01%   38.16%   +0.14%     
==========================================
  Files         239      239              
  Lines       15471    15500      +29     
  Branches     1353     1371      +18     
==========================================
+ Hits         5881     5915      +34     
+ Misses       9575     9570       -5     
  Partials       15       15              
Flag Coverage Δ
collection 100.00% <100.00%> (ø)
proxy 66.27% <ø> (ø)
rest 87.37% <ø> (ø)
ws 36.21% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@Jiralite Jiralite changed the title refactor(collection): optimisations perf(collection): optimisations Oct 11, 2024
@Jiralite Jiralite added this to the collection 2.2.0 milestone Oct 11, 2024
@almeidx
Copy link
Member

almeidx commented Oct 12, 2024

Should we add benchmarks for this? Would be easier to see the performance enhancements.

packages/collection/src/collection.ts Outdated Show resolved Hide resolved
packages/collection/src/collection.ts Outdated Show resolved Hide resolved
@kodiakhq kodiakhq bot merged commit ea04245 into discordjs:main Nov 7, 2024
24 checks passed
@Renegade334 Renegade334 deleted the collection-optimisations branch November 7, 2024 11:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

8 participants