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

Open
wants to merge 11 commits into
base: main
Choose a base branch
from

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 obviously 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 ot 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()

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 ~10-40% reduction in execution time when tested with all current LTS versions of Node, depending on the magnitude of the collection size and the number of elements requested.

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 Oct 13, 2024 5:54pm
discord-js-guide ⬜️ Ignored (Inspect) Visit Preview Oct 13, 2024 5:54pm

Copy link

codecov bot commented Oct 11, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 37.96%. Comparing base (24128a3) to head (b93437c).
Report is 9 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #10552      +/-   ##
==========================================
- Coverage   38.00%   37.96%   -0.05%     
==========================================
  Files         239      239              
  Lines       15488    15439      -49     
  Branches     1367     1364       -3     
==========================================
- Hits         5886     5861      -25     
+ Misses       9587     9563      -24     
  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.

Comment on lines +167 to +172
if (index >= 0) {
if (index >= this.size) return undefined;
} else {
if (index < this.size * -1) return undefined;
index += this.size;
}
Copy link
Contributor

@Syjalo Syjalo Oct 13, 2024

Choose a reason for hiding this comment

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

Better readability

Suggested change
if (index >= 0) {
if (index >= this.size) return undefined;
} else {
if (index < this.size * -1) return undefined;
index += this.size;
}
if (index < 0) {
index += this.size;
if (index < 0) return undefined;
} else if (index >= this.size) {
return undefined;
}

Copy link
Contributor

Choose a reason for hiding this comment

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

I think making one addition is faster then multiplication + addition

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, it's 14 8% faster for negative indexes

Comment on lines +191 to +196
if (index >= 0) {
if (index >= this.size) return undefined;
} else {
if (index < this.size * -1) return undefined;
index += this.size;
}
Copy link
Contributor

@Syjalo Syjalo Oct 13, 2024

Choose a reason for hiding this comment

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

Ditto

Suggested change
if (index >= 0) {
if (index >= this.size) return undefined;
} else {
if (index < this.size * -1) return undefined;
index += this.size;
}
if (index < 0) {
index += this.size;
if (index < 0) return undefined;
} else if (index >= this.size) {
return undefined;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Review in Progress
Development

Successfully merging this pull request may close these issues.

4 participants