-
Notifications
You must be signed in to change notification settings - Fork 30k
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
buffer: improve creation performance #6893
Conversation
cc @trevnorris as per request |
CI finished, I see only two failures in Windows build configs which are network timeouts. I guess unrelated? |
Hasn't Trevor already changed this between JS and C++ like 3 times? 😂 |
@Fishrock123 Well, it's mostly in JS, but the inheritance from TL;DR: the biggest win here is unrelated to the C++ change, but rather to the ES6 native subclassing. |
@nodejs/buffer |
I like the changes very much. Also makes it more readable... I'll test around |
Well, now Buffer.slice is very close to Uint8Array.subarray (I was comparing against it in my local benchmarks as a "theoretical maximum" which obviously can't be achieved in a wrapper, but can be very close (and it is now)). Please do let me know if I missed something / need to change before this can be merged. |
@@ -213,7 +214,7 @@ function allocate(size) { | |||
// Even though this is checked above, the conditional is a safety net and |
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.
Side note: this comment is irrelevant now, probably since dd67608, I overlooked it.
/cc @trevnorris
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.
Looks like it can be removed.
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.
Done.
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.
Looks like it has returned after a rebase =).
It's not critical, though, that could be removed later.
Not directly related, but now that I'm trying to submit changes from my Windows machine (previous one was from Mac), I've found one error: What would be the best fix for this - a PR that removes |
I think that discussion would best be taken to #6912 :) |
Oh cool, thanks! That's a new issue I haven't noticed yet :) |
Btw, can someone please explain what
is for / supposed to do? Just tried to wrap my head around it, and wasn't sure whether the additional semantics on top of simple |
There was some discussion on that in #2635… though I can’t seem to find the advantage of using |
Note that it only exists as part of a deprecated API anyway. |
Exactly my thoughts.
That's true, just looks pretty weird when trying to read / understand the code. |
|
||
// Regression test | ||
assert.doesNotThrow(() => { | ||
new Buffer(new ArrayBuffer()); |
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.
This should preferably use Buffer.from
instead of new Buffer
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.
Oh ok.
I’d maybe separate the LGTM either way. |
@RReverser Kinda… you don’t have to move that if you don’t want to, but keep in mind that the commit history ideally still makes sense for someone looking at it in a few years, without having the context of this PR in mind. Happens more often than you think. :) Also, it would be cool if you could re-format the commit message for that commit so that it adheres to the guidelines (i.e. it starts with |
Hm. If this PR addresses the comment removal, leave it in its own commit. Yes it's minor, but if this needs to be reverted for some unforeseen reason don't want the comment coming back in. As far as the regression, I vote we leave that to its own PR (since it'll require it's own regression test, etc.). |
@trevnorris Want to go ahead and land this then? |
if (size <= 0) | ||
return createBuffer(size); | ||
if (fill !== undefined) { | ||
if (size > 0 && fill !== undefined) { |
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.
Does a separate check for 0
make sense here, i.e. size > 0 && fill !== undefined && fill !== 0
?
Buffer.alloc(size, 0)
is equivalent to Buffer.alloc(size)
, so just new FastBuffer(size)
should work faster in that 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.
Hm, I think that would require benchmarking – createUnsafeBuffer()
returns a slice from the pool, so I’d actually expect that to be faster than an extra typed array allocation.
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.
@addaleax It's not just createUnsafeBuffer
, it's createUnsafeBuffer
+ fill(0)
.
I believe that has been discussed before, and allocation was proven to be faster — that's why simple Buffer.alloc(size)
does not use the pool.
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.
@ChALkeR I know there’s the extra fill()
in there. But if you say it’s faster, I believe 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.
@addaleax Btw, nothing in this function uses slices from the pool. Perhaps it should?
Both new FastBuffer
and createUnsafeBuffer
just directly allocate a new instance.
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.
Oh, right. Maybe, but I’d leave that open for another PR, too, especially as it would introduce the subtle change that the return values of Buffer.alloc()
would share their buffer
property.
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 down for that change. The kernel can probably optimize calls to calloc()
a hair better. Though not going to consider it a blocking change.
LGTM |
@addaleax Going ahead w/ these sounds good to me. |
Curious: would Buffer.alloc = function(size, fill, encoding) {
assertSize(size);
if (size > 0 && fill !== undefined && fill !== 0) {
if (typeof encoding !== 'string')
encoding = undefined;
return allocate(size).fill(fill, encoding);
}
return new FastBuffer(size);
}; be faster for short buffers filled with some non-zero argument? There are two changes here: |
On a second thought, we can do that in a separate PR, those are independent changes. |
I’m going to land this later today if nobody beats me to it. |
Yes, thought about similar further optimizations, but they would be rather backward-incompatible and cases where they give any win are more rare, so decided not to change. |
Improves performance of allocating unsafe buffers, creating buffers from an existing ArrayBuffer and creating .slice(...) from existing Buffer by avoiding deoptimizing change of prototype after Uint8Array allocation in favor of ES6 native subclassing. This is done through an internal ES6 class that extends Uint8Array and is used for allocations, but the regular Buffer function is exposed, so calling Buffer(...) with or without `new` continues to work as usual and prototype chains are also preserved. Performance wins for .slice are +120% (2.2x), and, consequently, for unsafe allocations up to +95% (1.9x) for small buffers, and for safe allocations (zero-filled) up to +30% (1.3x). PR-URL: #6893 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Сковорода Никита Андреевич <[email protected]>
Landed in 5292a13. Thanks for the contribution and for your patience with us! |
@addaleax Thank you! That was quite a trip, but totally fine as for the first PR to the project :) |
No arguing about that. 😄 If you like, you can also do PRs for some of the issues that popped up as side notes in the discussion here. If not, you don’t have to, of course.
… if I’ve managed to get everything right. ;) |
@addaleax Also |
@RReverser sounds good, yup :) |
@ChALkeR It was deliberate to not have var b;
while ((b = Buffer.allocUnsafe(1)).byteOffset > 0);
Buffer.from(b.buffer).fill(0);
setTimeout(() => {
// See what else has been written to the buffer since
console.log(b);
}, 3000); Can collect more information by messing with |
@trevnorris Ah, understood. I personally don't see how that is a problem, because But ok, let's keep it that way if there are concerns about that. Perhaps that should be documented as a small one-line comment in the source code? |
@ChALkeR That decision was simply my call when it was first implemented to make sure the PR would avoid additional scrutiny. If everyone's alright with using the pool then I won't stand in the way. |
@trevnorris On a second though, I think that you are correct here and that we should keep that as it is now. There could be various code errors on user side which could potentially cause issues if the code somehow uses the Also, the current behaviour is documented, and changing that would be a semver-major. So let's not change that =). |
This depends on #7082 and #7093, both of which have been marked dont-land-on-v6.x. @RReverser interested in opening a backport PR against the v6.x branch? |
#7176 (comment) same question here |
Improves performance of allocating unsafe buffers, creating buffers from an existing ArrayBuffer and creating .slice(...) from existing Buffer by avoiding deoptimizing change of prototype after Uint8Array allocation in favor of ES6 native subclassing. This is done through an internal ES6 class that extends Uint8Array and is used for allocations, but the regular Buffer function is exposed, so calling Buffer(...) with or without `new` continues to work as usual and prototype chains are also preserved. Performance wins for .slice are +120% (2.2x), and, consequently, for unsafe allocations up to +95% (1.9x) for small buffers, and for safe allocations (zero-filled) up to +30% (1.3x). PR-URL: #7349 Ref: #6893 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Сковорода Никита Андреевич <[email protected]> Reviewed-By: Trevor Norris <[email protected]>
Checklist
Affected core subsystem(s)
buffer
Description of change
Improves performance of allocating unsafe buffers, creating buffers from
an existing ArrayBuffer and creating .slice(...) from existing Buffer by
avoiding deoptimizing change of prototype after Uint8Array allocation
in favor of ES6 native subclassing.
This is done through an internal ES6 class that extends Uint8Array and
is used for allocations, but the regular Buffer function is exposed, so
calling Buffer(...) with or without
new
continues to work as usualand prototype chains are also preserved.
Performance wins for .slice are +120% (2.2x), and, consequently, for
unsafe allocations up to +95% (1.9x) for small buffers, and for safe
allocations (zero-filled) up to +30% (1.3x).