Skip to content

buffer: optimize Buffer.copy - #62491

Closed
ronag wants to merge 4 commits into
nodejs:mainfrom
ronag:faster-copy
Closed

buffer: optimize Buffer.copy#62491
ronag wants to merge 4 commits into
nodejs:mainfrom
ronag:faster-copy

Conversation

@ronag

@ronagronag commented Mar 29, 2026

Copy link
Copy Markdown
Member

This removes some of the overhead by extending the V8 api.

buffers/buffer-copy.js n=6000000 shared='false' partial='false' bytes=1024 *** 39.65 % ±1.24% ±1.66% ±2.20%
buffers/buffer-copy.js n=6000000 shared='false' partial='false' bytes=128 *** 62.40 % ±2.66% ±3.55% ±4.64%
buffers/buffer-copy.js n=6000000 shared='false' partial='false' bytes=8 *** 75.07 % ±1.82% ±2.42% ±3.15%
buffers/buffer-copy.js n=6000000 shared='false' partial='true' bytes=1024 *** 166.82 % ±2.74% ±3.69% ±4.90%
buffers/buffer-copy.js n=6000000 shared='false' partial='true' bytes=128 *** 224.72 % ±2.15% ±2.90% ±3.83%
buffers/buffer-copy.js n=6000000 shared='false' partial='true' bytes=8 *** 253.17 % ±1.71% ±2.29% ±3.00%
buffers/buffer-copy.js n=6000000 shared='true' partial='false' bytes=1024 *** 178.14 % ±13.28% ±17.89% ±23.75%
buffers/buffer-copy.js n=6000000 shared='true' partial='false' bytes=128 *** 105.29 % ±1.83% ±2.44% ±3.18%
buffers/buffer-copy.js n=6000000 shared='true' partial='false' bytes=8 *** 75.70 % ±1.72% ±2.29% ±2.98%
buffers/buffer-copy.js n=6000000 shared='true' partial='true' bytes=1024 *** 91.38 % ±10.05% ±13.54% ±17.98%
buffers/buffer-copy.js n=6000000 shared='true' partial='true' bytes=128 *** 230.69 % ±2.03% ±2.73% ±3.62%
buffers/buffer-copy.js n=6000000 shared='true' partial='true' bytes=8 *** 251.20 % ±1.84% ±2.47% ±3.24%
// PR
node$ out/Release/node benchmark/buffers/buffer-copy.js
buffers/buffer-copy.js n=6000000 shared="true" partial="true" bytes=8: 121,402,298.52590495
buffers/buffer-copy.js n=6000000 shared="false" partial="true" bytes=8: 150,598,474.5729912
buffers/buffer-copy.js n=6000000 shared="true" partial="false" bytes=8: 158,209,945.17735347
buffers/buffer-copy.js n=6000000 shared="false" partial="false" bytes=8: 146,067,437.3395647
buffers/buffer-copy.js n=6000000 shared="true" partial="true" bytes=128: 157,367,588.5504798
buffers/buffer-copy.js n=6000000 shared="false" partial="true" bytes=128: 146,945,070.7569905
buffers/buffer-copy.js n=6000000 shared="true" partial="false" bytes=128: 145,550,697.1271931
buffers/buffer-copy.js n=6000000 shared="false" partial="false" bytes=128: 137,749,082.19508398
buffers/buffer-copy.js n=6000000 shared="true" partial="true" bytes=1024: 86,024,692.29827811
buffers/buffer-copy.js n=6000000 shared="false" partial="true" bytes=1024: 82,854,617.76403005
buffers/buffer-copy.js n=6000000 shared="true" partial="false" bytes=1024: 65,355,531.58653537
buffers/buffer-copy.js n=6000000 shared="false" partial="false" bytes=1024: 57,637,071.98025907
// v25.6.1
node$ node benchmark/buffers/buffer-copy.js
buffers/buffer-copy.js n=6000000 shared="true" partial="true" bytes=8: 42,373,280.25128937
buffers/buffer-copy.js n=6000000 shared="false" partial="true" bytes=8: 44,768,824.757315286
buffers/buffer-copy.js n=6000000 shared="true" partial="false" bytes=8: 68,458,244.01386568
buffers/buffer-copy.js n=6000000 shared="false" partial="false" bytes=8: 86,251,974.36160062
buffers/buffer-copy.js n=6000000 shared="true" partial="true" bytes=128: 43,440,905.782851204
buffers/buffer-copy.js n=6000000 shared="false" partial="true" bytes=128: 46,963,775.47802432
buffers/buffer-copy.js n=6000000 shared="true" partial="false" bytes=128: 53,035,118.51192226
buffers/buffer-copy.js n=6000000 shared="false" partial="false" bytes=128: 85,299,162.71621366
buffers/buffer-copy.js n=6000000 shared="true" partial="true" bytes=1024: 33,336,852.223290235
buffers/buffer-copy.js n=6000000 shared="false" partial="true" bytes=1024: 40,153,531.31000715
buffers/buffer-copy.js n=6000000 shared="true" partial="false" bytes=1024: 16,149,801.988758216
buffers/buffer-copy.js n=6000000 shared="false" partial="false" bytes=1024: 49,143,503.67479149

@ronag
ronag requested review from mcollina and targosMarch 29, 2026 08:24
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/security-wg
  • @nodejs/v8-update

@nodejs-github-botnodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. labels Mar 29, 2026
@ronag
ronag requested a review from joyeecheungMarch 29, 2026 08:27
@ronag

Copy link
Copy Markdown
MemberAuthor

@nodejs/performance

This removes some of the overhead by extending the V8 api.
PR-URL: nodejs#62491
Comment threaddeps/v8/src/api/api.cc

@mcollinamcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm

@mcollinamcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Mar 29, 2026
Comment threaddeps/v8/include/v8-array-buffer.h Outdated
@github-actionsgithub-actionsBot added request-ci-failed An error occurred while starting CI via request-ci label, and manual interventon is needed. and removed request-ci Add this label to start a Jenkins CI on a PR. labels Mar 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor
Failed to start CI
 ⚠ Commits were pushed since the last approving review:
⚠ - buffer: optimize Buffer.copy
⚠ - Update v8-array-buffer.h
✘ Refusing to run CI on potentially unsafe PR
https://github.com/nodejs/node/actions/runs/23705961064

@ronagronag added the author ready PRs that have at least one approval, no pending requests for changes, and a CI started. label Mar 29, 2026
@ronag

Copy link
Copy Markdown
MemberAuthor

If you feel strongly about the check please open a pr and fix it properly across all the other methods as well. As you wrote in the issue. Messing around with prototypes is not supported.

@joyeecheung

Copy link
Copy Markdown
Member

Can you upstream the V8 changes first? In general floating patches is reserved for toolchains/platforms V8 doesn't support, that adds maintenance burden to rebase the patches during V8 updates especially when V8 does any architectural changes (e.g. they are constantly changing how handles should be used internally, which can easily turns patches like this an upgrade blocker), so it's always better to merge them first in the upstream.

Comment threaddeps/v8/include/v8-array-buffer.h Outdated
@ronag

Copy link
Copy Markdown
MemberAuthor

@ronag

ronag commented Mar 29, 2026

Copy link
Copy Markdown
MemberAuthor

an you upstream the V8 changes first?

I don't think it's likely to land and I don't know how to upstream to V8. If that's a blocker for you then this PR is moot. I appreciate the maintenance burden and if those that have to deal with it object then I have no objection to closing this PR.

@joyeecheungjoyeecheung left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this needs to be upstreamed first. I could catch the pointer issue from a glance. But I don't think I know enough to gauge the internal API uses are safe and along the lines of where V8 is going without review from people in the V8 team. Floating patches can both add maintenance burden to our V8 upgrades as well as V8's integration CI (they build Node.js with different configurations there e.g. with pointer compression and could get hit by a different set of problems). They are generally reserved for things that V8 defer to others (MSVC support or niche platform support - even in those cases we still try to upstream to minimize the burden, like the big endian patches), not things like features or performance optimizations.

@ronagronag closed this Mar 29, 2026
@ronag

ronag commented Mar 29, 2026

Copy link
Copy Markdown
MemberAuthor

Understood. Unfortunate. Was a significant perf boost.

ronag added a commit to ronag/node that referenced this pull request Mar 29, 2026
This removes some of the overhead by extending the V8 api.
PR-URL: nodejs#62491
@ronag

ronag commented Mar 29, 2026

Copy link
Copy Markdown
MemberAuthor

Benchmark results:

buffers/buffer-copy.js n=6000000 shared='false' partial='false' bytes=1024 *** 39.65 % ±1.24% ±1.66% ±2.20%
buffers/buffer-copy.js n=6000000 shared='false' partial='false' bytes=128 *** 62.40 % ±2.66% ±3.55% ±4.64%
buffers/buffer-copy.js n=6000000 shared='false' partial='false' bytes=8 *** 75.07 % ±1.82% ±2.42% ±3.15%
buffers/buffer-copy.js n=6000000 shared='false' partial='true' bytes=1024 *** 166.82 % ±2.74% ±3.69% ±4.90%
buffers/buffer-copy.js n=6000000 shared='false' partial='true' bytes=128 *** 224.72 % ±2.15% ±2.90% ±3.83%
buffers/buffer-copy.js n=6000000 shared='false' partial='true' bytes=8 *** 253.17 % ±1.71% ±2.29% ±3.00%
buffers/buffer-copy.js n=6000000 shared='true' partial='false' bytes=1024 *** 178.14 % ±13.28% ±17.89% ±23.75%
buffers/buffer-copy.js n=6000000 shared='true' partial='false' bytes=128 *** 105.29 % ±1.83% ±2.44% ±3.18%
buffers/buffer-copy.js n=6000000 shared='true' partial='false' bytes=8 *** 75.70 % ±1.72% ±2.29% ±2.98%
buffers/buffer-copy.js n=6000000 shared='true' partial='true' bytes=1024 *** 91.38 % ±10.05% ±13.54% ±17.98%
buffers/buffer-copy.js n=6000000 shared='true' partial='true' bytes=128 *** 230.69 % ±2.03% ±2.73% ±3.62%
buffers/buffer-copy.js n=6000000 shared='true' partial='true' bytes=8 *** 251.20 % ±1.84% ±2.47% ±3.24%

@codecov

codecovBot commented Mar 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 58.33333% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.70%. Comparing base (cc96741) to head (7da38b1).
⚠️ Report is 1 commits behind head on main.

Files with missing linesPatch %Lines
src/node_buffer.cc50.00%5 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #62491 +/- ##
==========================================
- Coverage 91.55% 89.70% -1.85% 
==========================================
Files 351 692 +341 Lines 147653 213981 +66328 Branches 23224 41050 +17826 ==========================================
+ Hits 135179 191948 +56769 - Misses 12217 14110 +1893 - Partials 257 7923 +7666 
Files with missing linesCoverage Δ
lib/buffer.js99.16% <100.00%> (+12.66%)⬆️
src/node_buffer.cc68.07% <50.00%> (ø)

... and 461 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jasnell

Copy link
Copy Markdown
Member

I like it, but yeah, I agree with @joyeecheung ... an attempt to upstream this into v8 should be made first.

@ChALkeR

ChALkeR commented Mar 29, 2026

Copy link
Copy Markdown
Member

@ronag

The check is done in js.

It isn't robust (as I mentioned above).

Doing checks in fast api context is difficult since as far as I'm aware you can't throw from there.

You can crash the process (again, as mentioned), which is better than reading/writing arbitrary process memory.

@ronag

ronag commented Mar 29, 2026

Copy link
Copy Markdown
MemberAuthor

I've funded work to upstream this to V8. Hopefully this will be able to land in the future without blocking nits.

@ronag

ronag commented Apr 7, 2026

Copy link
Copy Markdown
MemberAuthor

@joyeecheung@jasnell I funded work to get this upstreamed, see https://chromium-review.googlesource.com/c/v8/v8/+/7735151

However, in order to land it in V8 we would probably need to use Relaxed_Memmove to keep it consistent with the V8 memory model, which as document in this PR is different than the node memory model. So I don't think we can upstream this to node and keep the perf improvement for SharedArrayBuffers.

Is floating a patch still off the table?

@ronag

ronag commented Apr 7, 2026

Copy link
Copy Markdown
MemberAuthor

Alternatively, if there is some way to "automagically" replace base::Relaxed_Memmove with a regular memmove. Maybe a build step?

ronag added a commit to ronag/node that referenced this pull request Jun 10, 2026
Route the native backing of Buffer.prototype.copy (CopyImpl, the `_copy`
binding) through V8's new v8::ArrayBuffer::CopyArrayBufferBytes API
(added in the preceding commit) instead of materializing an
ArrayBufferViewContents and doing a manual memmove. This speeds up
partial copies (sourceStart > 0). All copies now go through this
binding: the previous %TypedArray%.prototype.set fast-path for
whole-buffer copies is dropped, since it would throw on a detached or
immutable target rather than report a 0-byte no-op, and the benchmarks
below show the native path is comparable for that case.
When both sides are backed by a SharedArrayBuffer the relaxed-atomic
overload is used, which honors the SharedArrayBuffer memory model.
Mixed copies (one SharedArrayBuffer, one ArrayBuffer) use the regular
overload.
copy() now reports the number of bytes actually copied, as returned by
V8: 0 when the target is backed by a detached or immutable ArrayBuffer.
The copy is then a no-op rather than a write to read-only memory.
The native binding now plumbs byte offsets and the copied-byte count
through as size_t, passed across the fast/slow API boundary as doubles
(exact for integer values below 2^53), so copies that cross the 4 GiB
boundary are no longer truncated to 32 bits.
buffer-copy.js vs node v26.3.0 (x64, 30 runs):
partial=false bytes=1024: +7.91% (***)
partial=false bytes=128: +0.17%
partial=false bytes=8: -0.89%
partial=true bytes=1024: +22.33% (***)
partial=true bytes=128: +19.58% (***)
partial=true bytes=8: +18.70% (***)
This supersedes the prototype in
nodejs#62491, which added a bespoke
ArrayBufferView::FastCopy instead of using the upstream-friendly
CopyArrayBufferBytes API.
Adds SharedArrayBuffer and immutable-ArrayBuffer coverage to the buffer
copy tests, plus a pummel regression test for copies larger than 2^32
bytes.
Refs: nodejs#55422
Signed-off-by: Robert Nagy <ronagy@icloud.com>
Assisted-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
nodejs-github-bot pushed a commit that referenced this pull request Jun 12, 2026
Route the native backing of Buffer.prototype.copy (CopyImpl, the `_copy`
binding) through V8's new v8::ArrayBuffer::CopyArrayBufferBytes API
(added in the preceding commit) instead of materializing an
ArrayBufferViewContents and doing a manual memmove. This speeds up
partial copies (sourceStart > 0). All copies now go through this
binding: the previous %TypedArray%.prototype.set fast-path for
whole-buffer copies is dropped, since it would throw on a detached or
immutable target rather than report a 0-byte no-op, and the benchmarks
below show the native path is comparable for that case.
When both sides are backed by a SharedArrayBuffer the relaxed-atomic
overload is used, which honors the SharedArrayBuffer memory model.
Mixed copies (one SharedArrayBuffer, one ArrayBuffer) use the regular
overload.
copy() now reports the number of bytes actually copied, as returned by
V8: 0 when the target is backed by a detached or immutable ArrayBuffer.
The copy is then a no-op rather than a write to read-only memory.
The native binding now plumbs byte offsets and the copied-byte count
through as size_t, passed across the fast/slow API boundary as doubles
(exact for integer values below 2^53), so copies that cross the 4 GiB
boundary are no longer truncated to 32 bits.
buffer-copy.js vs node v26.3.0 (x64, 30 runs):
partial=false bytes=1024: +7.91% (***)
partial=false bytes=128: +0.17%
partial=false bytes=8: -0.89%
partial=true bytes=1024: +22.33% (***)
partial=true bytes=128: +19.58% (***)
partial=true bytes=8: +18.70% (***)
This supersedes the prototype in
#62491, which added a bespoke
ArrayBufferView::FastCopy instead of using the upstream-friendly
CopyArrayBufferBytes API.
Adds SharedArrayBuffer and immutable-ArrayBuffer coverage to the buffer
copy tests, plus a pummel regression test for copies larger than 2^32
bytes.
Refs: #55422
Signed-off-by: Robert Nagy <ronagy@icloud.com>
Assisted-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PR-URL: #63828
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: René <contact.9a5d6388@renegade334.me.uk>
aduh95 pushed a commit that referenced this pull request Jun 18, 2026
Route the native backing of Buffer.prototype.copy (CopyImpl, the `_copy`
binding) through V8's new v8::ArrayBuffer::CopyArrayBufferBytes API
(added in the preceding commit) instead of materializing an
ArrayBufferViewContents and doing a manual memmove. This speeds up
partial copies (sourceStart > 0). All copies now go through this
binding: the previous %TypedArray%.prototype.set fast-path for
whole-buffer copies is dropped, since it would throw on a detached or
immutable target rather than report a 0-byte no-op, and the benchmarks
below show the native path is comparable for that case.
When both sides are backed by a SharedArrayBuffer the relaxed-atomic
overload is used, which honors the SharedArrayBuffer memory model.
Mixed copies (one SharedArrayBuffer, one ArrayBuffer) use the regular
overload.
copy() now reports the number of bytes actually copied, as returned by
V8: 0 when the target is backed by a detached or immutable ArrayBuffer.
The copy is then a no-op rather than a write to read-only memory.
The native binding now plumbs byte offsets and the copied-byte count
through as size_t, passed across the fast/slow API boundary as doubles
(exact for integer values below 2^53), so copies that cross the 4 GiB
boundary are no longer truncated to 32 bits.
buffer-copy.js vs node v26.3.0 (x64, 30 runs):
partial=false bytes=1024: +7.91% (***)
partial=false bytes=128: +0.17%
partial=false bytes=8: -0.89%
partial=true bytes=1024: +22.33% (***)
partial=true bytes=128: +19.58% (***)
partial=true bytes=8: +18.70% (***)
This supersedes the prototype in
#62491, which added a bespoke
ArrayBufferView::FastCopy instead of using the upstream-friendly
CopyArrayBufferBytes API.
Adds SharedArrayBuffer and immutable-ArrayBuffer coverage to the buffer
copy tests, plus a pummel regression test for copies larger than 2^32
bytes.
Refs: #55422
Signed-off-by: Robert Nagy <ronagy@icloud.com>
Assisted-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PR-URL: #63828
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: René <contact.9a5d6388@renegade334.me.uk>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author readyPRs that have at least one approval, no pending requests for changes, and a CI started.c++Issues and PRs that require attention from people who are familiar with C++.lib / srcIssues and PRs related to general changes in the lib or src directory.needs-ciPRs that need a full CI run.tsc-agendaIssues and PRs to discuss during the meetings of the TSC.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@ronag@nodejs-github-bot@joyeecheung@jasnell@ChALkeR@mcollina