-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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
Optimize reduce
loops - late 2023 edition
#168812
Conversation
reduce
loops - 2023 editionreduce
loops - late 2023 edition
💔 Build FailedFailed CI Steps
Test Failures
Metrics [docs]Async chunks
Page load bundle
Unknown metric groupsESLint disabled line counts
Total ESLint disabled count
History
To update your PR or re-run it, just comment with: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ent-search changes LGTM
@@ -117,7 +117,10 @@ export async function getSeriesData( | |||
[panel.id]: { | |||
annotations, | |||
id: panel.id, | |||
series: series.reduce((acc, s) => acc.concat(s), []), | |||
series: series.reduce((acc, s) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did profile this already and concat
is way faster than push
+ spread.
In terms of GCC pressure both approach will create a new Array and clean up but the concat
approach avoids to iterate on the second one.
Surprisingly Chrome
fails to execute on the push + spread
on my machine. But concat
in Chrome is super fast.
Safari seems to be faster with push + spread
, with concat
second.
Firefox is way faster with concat
.
They are still micro-benchmarks, so probably it is not worth push more here, but my point is that concat
is "fast enough" already and there's no need to make the code more complex in this case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you tried on a real server-side environment? The results from my personal benchmarks are surprisingly different between chrome/firefox and nodejs.
Now if you think some changes are unnecessary, I'm fine with it. would that be the only file you want me to revert?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you report some results from the nodejs benchmarks?
As I said, if the perf toll is not dramatically higher with concat
I would rather keep this version that the other. Otherwise I think it is ok to change it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I've taken the benchmarking code below and made it more generic to test various scenarios:
const Benchmark = require('benchmark');
function createDataset(series, length){
return Array.from({length: series}).fill(Array.from({length}).map((_, i)=>`${i}`)).map((v, i) => [i, v]).sort(([a], [b]) => a - b);
}
for( const dataset of [createDataset(3, 3), createDataset(3, 100), createDataset(100, 3), createDataset(100, 100), createDataset(100, 1000)]){
console.log(`${Array(10).fill('-').join('')} Series: ${dataset.length} - Length: ${dataset[0][1].length} ${Array(10).fill('-').join('')}`)
const suite = new Benchmark.Suite(`Series: ${dataset.length} - Length: ${dataset[0].length}`);
suite
.add('flatMap', function () {
const out1 = dataset.flatMap(([, result]) => result);
})
.add('reduce with concat', function () {
const out1 = dataset.reduce((acc, [, result]) => acc.concat(result), []);
})
.add('reduce with push', function () {
const out1 = dataset.reduce((acc, [, result]) => {
acc.push(...result);
return acc;
}, []);
})
.add('map with flat', function () {
const out1 = dataset.map(([, result]) => result).flat();
})
.add('concat with map', function () {
const out1 = [].concat(...dataset.map(([, result]) => result));
});
suite
// add listeners
.on('cycle', function (event) {
console.log(String(event.target));
})
.on('complete', function () {
console.log('Fastest is ' + this.filter('fastest').map('name'));
});
suite.run();
}
So I've tested different dimension scenarios and added some more reduce/concat tricks.
The result were interesting:
---------- Series: 3 - Length: 3 ----------
flatMap x 1,229,080 ops/sec ±0.11% (101 runs sampled)
reduce with concat x 5,801,331 ops/sec ±0.09% (99 runs sampled)
reduce with push x 16,493,742 ops/sec ±0.29% (99 runs sampled)
map with flat x 1,166,655 ops/sec ±0.08% (100 runs sampled)
concat with map x 8,050,295 ops/sec ±0.32% (99 runs sampled)
Fastest is reduce with push
---------- Series: 3 - Length: 100 ----------
flatMap x 49,539 ops/sec ±0.08% (98 runs sampled)
reduce with concat x 2,806,867 ops/sec ±0.24% (97 runs sampled)
reduce with push x 1,464,221 ops/sec ±0.17% (98 runs sampled)
map with flat x 49,311 ops/sec ±0.07% (97 runs sampled)
concat with map x 4,517,977 ops/sec ±0.15% (97 runs sampled)
Fastest is concat with map
---------- Series: 100 - Length: 3 ----------
flatMap x 41,307 ops/sec ±0.12% (98 runs sampled)
reduce with concat x 79,167 ops/sec ±0.12% (99 runs sampled)
reduce with push x 450,069 ops/sec ±0.14% (96 runs sampled)
map with flat x 40,161 ops/sec ±0.07% (101 runs sampled)
concat with map x 443,343 ops/sec ±0.14% (100 runs sampled)
Fastest is reduce with push
---------- Series: 100 - Length: 100 ----------
flatMap x 1,490 ops/sec ±0.07% (99 runs sampled)
reduce with concat x 4,846 ops/sec ±0.44% (100 runs sampled)
reduce with push x 33,905 ops/sec ±0.16% (101 runs sampled)
map with flat x 1,488 ops/sec ±0.06% (99 runs sampled)
concat with map x 173,222 ops/sec ±0.17% (97 runs sampled)
Fastest is concat with map
---------- Series: 100 - Length: 1000 ----------
flatMap x 144 ops/sec ±0.09% (83 runs sampled)
reduce with concat x 163 ops/sec ±0.79% (85 runs sampled)
reduce with push x 1,903 ops/sec ±0.64% (86 runs sampled)
map with flat x 144 ops/sec ±0.19% (83 runs sampled)
concat with map x 9,167 ops/sec ±0.69% (92 runs sampled)
Fastest is concat with map
So I've learned few things:
concat
is not that fast within areduce
as I have imagined in nodepush
is faster then the given array spread is shortarguments
spread is way faster than I thought initially for longer arrays (tens of items)
So ok with the push
version for now, but perhaps I might refactor later critical parts using the [].concat
trick 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
.reduce((acc: T2[], [, result]) => { | ||
acc.push(...result); | ||
return acc; | ||
}, []); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Running a benchmark on NodeJS shows reduce with push
is best
flatMap x 1,226,284 ops/sec ±0.40% (97 runs sampled)
reduce with concat x 6,033,030 ops/sec ±0.19% (101 runs sampled)
reduce with push x 16,624,071 ops/sec ±0.36% (93 runs sampled)
Fastest is reduce with push
This is the piece of code I ran:
const Benchmark = require('benchmark');
const suite = new Benchmark.Suite;
const results = [
[0, ["a", "b", "c"]],
[2, ["g", "h", "i"]],
[1, ["d", "e", "f"]],
].sort(([a], [b]) => a - b);
suite
.add('flatMap', function () {
const out1 = results.flatMap(([, result]) => result);
})
.add('reduce with concat', function () {
const out1 = results.reduce((acc, [, result]) => acc.concat(result), []);
})
.add('reduce with push', function () {
const out1 = results.reduce((acc, [, result]) => {
acc.push(...result);
return acc;
}, []);
});
suite
// add listeners
.on('cycle', function (event) {
console.log(String(event.target));
})
.on('complete', function () {
console.log('Fastest is ' + this.filter('fastest').map('name'));
});
suite.run();
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Enterprise Search looks good, and we thank you <3
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ResponseOps changes LGTM
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ML team related changes LGTM. 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OPs changes lgtm
return { | ||
...p, | ||
[name]: p[name].concat({ | ||
const updatedSeries = kpiNames.reduce( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
chapeau for being able to read this code 😄 (it's mine so I can say that)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fleet changes LGTM
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fleet changes LGTM!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Defend Workflows changes LGTM 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Profiling changes LGTM
(acc, [, privilegeSet]) => new Set([...acc, ...privilegeSet]), | ||
(acc, [, privilegeSet]) => { | ||
privilegeSet.forEach((entry) => acc.add(entry)); | ||
return acc; | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure these are functionally identical. The Set
was used to enforce uniqueness from my understanding.
@@ -25,7 +25,8 @@ export function snip(source: string, snips: Snip[]) { | |||
.reduce((acc: Snip[], s) => { | |||
const prev = acc.at(-1); | |||
if (!prev || prev[1] < s[0]) { | |||
return [...acc, s]; | |||
acc.push(s); | |||
return acc; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a similar spread a few lines below. In the light of total elimination, should that be replaced by something like:
const merged: Snip = [Math.min(prev[0], s[0]), Math.max(prev[1], s[1])];
acc[acc.length-1] = merged;
return acc;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Operations 👍
}, {} as EsQueryConfig); | ||
return paramKeysWithValues; | ||
return acc; | ||
}, {} as Record<string, unknown>); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
castings like this in reduce can be avoided by passing interface in reduce generic
const paramKeysWithValues = Object.keys(params).reduce<Record<string, unknown>>((acc, key) => {
@@ -18,6 +22,9 @@ export const sortWithExcludesAtEnd = (indices: string[]) => { | |||
export const ensurePatternFormat = (patternList: string[]): string[] => | |||
sortWithExcludesAtEnd([ | |||
...new Set( | |||
patternList.reduce((acc: string[], pattern: string) => [...pattern.split(','), ...acc], []) | |||
patternList.reduce((acc: string[], pattern: string) => { | |||
acc.unshift(...pattern.split(',')); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interestingly, is unshift
significant better here to use?
unshift
adds element to beginning of array, thus having O(n).
But readability is a bit better in original code with spread
...agg, | ||
[_index]: signalsVersions, | ||
}; | ||
(agg as Record<string, unknown>)[_index] = signalsVersions; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think casting here also can be resolved by using generic
@@ -193,6 +194,7 @@ export const convertToSnakeCase = <T extends Record<string, unknown>>( | |||
} | |||
return Object.keys(obj).reduce((acc, item) => { | |||
const newKey = snakeCase(item); | |||
return { ...acc, [newKey]: obj[item] }; | |||
(acc as Record<string, unknown>)[newKey] = obj[item]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think casting here also can be resolved by using generic
return foundListsResponse.data.reduce((acc, list) => { | ||
// eslint-disable-next-line no-param-reassign | ||
acc[list.list_id] = list; | ||
return acc; | ||
}, {} as Record<string, ExceptionListSchema>); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return foundListsResponse.data.reduce((acc, list) => { | |
// eslint-disable-next-line no-param-reassign | |
acc[list.list_id] = list; | |
return acc; | |
}, {} as Record<string, ExceptionListSchema>); | |
return foundListsResponse.data.reduce<Record<string, ExceptionListSchema>>((acc, list) => { | |
// eslint-disable-next-line no-param-reassign | |
acc[list.list_id] = list; | |
return acc; | |
}, {}); |
return transformedResponse.data.reduce((acc, item) => { | ||
// eslint-disable-next-line no-param-reassign | ||
acc[item.item_id] = item; | ||
return acc; | ||
}, {} as Record<string, ExceptionListItemSchema>); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return transformedResponse.data.reduce((acc, item) => { | |
// eslint-disable-next-line no-param-reassign | |
acc[item.item_id] = item; | |
return acc; | |
}, {} as Record<string, ExceptionListItemSchema>); | |
return transformedResponse.data.reduce<Record<string, ExceptionListItemSchema>>((acc, item) => { | |
// eslint-disable-next-line no-param-reassign | |
acc[item.item_id] = item; | |
return acc; | |
}, {}); |
Closing as won't fix |
## Summary Cherry picked from #168812 the changes from `core` packages and the `security` and `spaces` plugins --------- Co-authored-by: Kibana Machine <[email protected]>
Summary
Follow-up of #157471, see original PR for more context.
Don't expect the same perf gain though, I don't think this PR impacted any bottlenecks, as the last one did.
Still, faster is always better, and you become the GC's BFF