-
Notifications
You must be signed in to change notification settings - Fork 29.7k
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
http2: use util._extend
instead of Object.assign
#18766
Conversation
These aren't statistically significant results, unfortunately. Most of the http2 benchmarks have a huge amount of fluctuation (the simple.js one in particular can easily fluctuate as much as 100% between two runs) and need to be run a minimum 100 times, ideally even more, to get any sort of significant data. You can use I'll trigger our benchmark CI job for this but my initial expectation is that this will only make a significant difference in the http2/headers benchmark with 1000 headers. The fact that the ordering of the keys changes could be problematic. This also means that Node won't be able to benefit from any future improvements of |
thanks for triggering the CI @apapirovski Also I was about to open a new Issue to ask about the ordering or the |
I'm not a fan of making changes to deprecated code. And yes, it likely is a breaking change. As unlikely as it is, someone could rely on the exact order. |
In this case you only ran
One question, why didnt you run the other 3 benchmarks that there are for http2? ( |
@jvelezpo It looks like we can't run those since
|
Yeah, the CI does not yet have everything it needs for those. /cc @nodejs/build I'll get an issue opened in the build repo for that. For this particular PR, I'm not quite sold on this as I would prefer not to continue using the deprecated and non-idiomatic |
@jasnell @apapirovski the function got deprecated to make sure it is not used outside of Node.js. I personally feel like we should use it until |
@BridgeAR Well, the semantics aren't strictly the same either as evidenced by the tests needing to change. I don't feel like the upside is worth the change here. I could be wrong... |
@apapirovski the tests rely on the order. It is a bit weird that the keys get inserted from behind in _extend. I would normally say, let us just switch to the regular order. It is used quite frequently throughout /lib though and that would would be a much bigger breakage. We might do that nevertheless and just declare this as a semver-major? |
@BridgeAR and @apapirovski i did a test on my local changing the
Also we dont know outside of node as @apapirovski says maybe this can be another PR 🤔 |
We can also add another internal extend function that has the proper order. |
Another internal |
Keep in mind that it is entirely possible (and likely) that the reverse order iteration is small part of the reason why this method may be faster.node This is just a quick performance.timerify spot check with an object with 100 properties and not a proper benchmark, but reversing the order of the iteration definitely appears to have an impact. Durations of multiple runs with forward iteration
Durations of multiple runs of util._extend
|
@jasnell I just checked and all my variants also show a different performance profile. @bmeurer I am a bit surprised that iterating down is faster than iterating up. Is there a specific reason for that? Example code: function _extend1(target, source) {
if (source === null || typeof source !== 'object') return target;
const keys = Object.keys(source);
var i = keys.length;
while (i--) {
target[keys[i]] = source[keys[i]];
}
return target;
}
function _extend2(target, source) {
if (source === null || typeof source !== 'object') return target;
const keys = Object.keys(source);
var i = 0;
while (i < keys.length) {
target[keys[i]] = source[keys[i++]];
}
return target;
}
function bench(fn) {
console.time(`Runtime ${fn.name}`);
for (var i = 0; i < 4e5; i++) {
input[i % 100] = -input[i % 100];
fn({}, input);
}
console.timeEnd(`Runtime ${fn.name}`);
}
const input = {};
for (var i = 0; i < 20; i++) {
input['a' + (i % 2 * -1)] = 'foo' + i;
}
for (i = 0; i < 20; i++) {
bench(_extend2);
bench(_extend1);
} |
@BridgeAR I think that the part of the answer, is that in this case you cache the length of the array, so each time it loops, it doesn't have to calc the length of it.... But also the reverse loop or iterating down in most of the cases is faster than the iterating up, here is a good benchmark to check it in jsperf |
V8 is able to cache the length according to @bmeurer so I don't think that's the issue. Doing a quick test in repl reveals that to be the case. As far as I can tell, iterating backwards and forwards has equivalent performance now. Which means that this must be related to some sort of property access caching or something, where retrieving the most recently stored values and going backwards is faster than the the other way around. |
jsperf is a pretty awful tool IMO for measuring benchmark results. I'd like feedback from V8 on why It would also really surprise me if these cases cause any measurable difference (just copying settings around). Moreover, I'm not even sure why we create a copy in some of these places like in https://github.com/nodejs/node/pull/18766/files#diff-696b2cc418addca5f3fe5020058f8b15R678 . |
Check out the benchmark folder. @jasnell added a benchmark recently, it shows that both spread & Object.assign are significantly slower. ping @bmeurer re: |
Agree, there are some places where a copy does not make sense, i will make a change there in a bit. |
@apapirovski see #16081 (comment) In the tracking bug there is a comment that someone recently got assigned to work on this :-) |
@BridgeAR Ah, thanks. Hadn't seen that :) |
@apapirovski first of all - it's nice that we have a benchmark for that (so props jasnell). I'm not sure I agree with it entirely though :P If you look at https://github.com/nodejs/node/blob/master/benchmark/misc/util-extend-vs-object-assign.js#L28 it calls it with a proxy object, Object.assign has different set behavior from object spread and util._extend as far as I know so it's different behavior rather than just different spread. If you meant another benchmark I'd love to see it. On a tangent - is there any reason we don't use V8 natives syntax in benchmarks? The way we "force optimize" is flakey and V8 provides a function to %OptimizeFunctionOnNextCall - the type feedback we're giving it (process.env) depends on the running environment too which isn't too great. |
|
@benjamingr did the change in the line you suggested and i get this error while testing. It is there to create an empty object when
|
How shall we progress here? |
For now I think I'm -1 on this change. |
I am closing this due to the mentioned concerns. @jvelezpo thanks a lot for your contribution anyway! It is much appreciated. |
Since
util._extend()
still wins currently overObject.assign
it is better to use it in terms of performance here in http2
Benchmark before
Benchmark after
Refs: #18707
Refs: #18442
Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passesAffected core subsystem(s)
http2, test