Commit 615273d

Browse files
ronagclaude
authored andcommitted
buffer: support aligned allocations
Add an optional `alignment` argument to `Buffer.allocUnsafe()` and `Buffer.allocUnsafeSlow()`, which guarantees that the memory backing the returned buffer starts at an address that is a multiple of `alignment`. Some operating system interfaces refuse to work with unaligned memory. The motivating case is unbuffered ("direct") file I/O: a read or write on a descriptor opened with `O_DIRECT` fails with `EINVAL` unless the buffer address is a multiple of the logical block size of the underlying device. Until now there was no way to obtain such a buffer from JS, since the address of a backing store can neither be observed nor chosen. Alignment is also worth having purely for performance, for instance to keep a hot buffer from straddling one more cache line than its size requires. V8 does not allow picking the address of a backing store, so alignment is instead achieved by over-allocating `alignment - 1` bytes and positioning the buffer at the first suitably aligned byte within them. The new `arrayBufferAlignedOffset()` binding computes that offset. Addresses are not stable across snapshot serialization, so the binding reports no padding while a snapshot is being built. Otherwise the snapshot would capture where this particular process happened to allocate and stop being reproducible. Buffers restored from a snapshot are consequently not aligned; the pool works around this by recreating itself in a deserialize callback. The `Buffer.allocUnsafe()` pool is now aligned to a cache line itself, which lets pooled allocations satisfy any alignment up to 64 bytes by padding their offset into the pool rather than allocating separately. Assisted-by: Claude/Opus 5 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Robert Nagy <ronagy@icloud.com> PR-URL: #65003 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
1 parent 49fb028 commit 615273d

7 files changed

Lines changed: 415 additions & 35 deletions

File tree

β€Ždoc/api/buffer.mdβ€Ž

Lines changed: 94 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -791,11 +791,14 @@ data that might not have been allocated for `Buffer`s.
791791

792792
A `TypeError` will be thrown if `size` is not a number.
793793

794-
### Static method: `Buffer.allocUnsafe(size)`
794+
### Static method: `Buffer.allocUnsafe(size[, alignment])`
795795

796796
<!-- YAML
797797
added: v5.10.0
798798
changes:
799+
- version: REPLACEME
800+
pr-url: https://github.com/nodejs/node/pull/65003
801+
description: Added the `alignment` argument.
799802
- version: v20.0.0
800803
pr-url: https://github.com/nodejs/node/pull/45796
801804
description: Throw ERR_INVALID_ARG_TYPE or ERR_OUT_OF_RANGE instead of
@@ -810,6 +813,9 @@ changes:
810813
-->
811814

812815
*`size` {integer} The desired length of the new `Buffer`.
816+
*`alignment` {integer} If given, the memory backing the new `Buffer` will start
817+
at an address that is a multiple of `alignment`. Must be a power of two no
818+
larger than `2 ** 30`. See [Aligned allocations][].
813819
* Returns: {Buffer}
814820

815821
Allocates a new `Buffer` of `size` bytes. If `size` is larger than
@@ -865,11 +871,14 @@ pool, while `Buffer.allocUnsafe(size).fill(fill)` _will_ use the internal
865871
difference is subtle but can be important when an application requires the
866872
additional performance that [`Buffer.allocUnsafe()`][] provides.
867873

868-
### Static method: `Buffer.allocUnsafeSlow(size)`
874+
### Static method: `Buffer.allocUnsafeSlow(size[, alignment])`
869875

870876
<!-- YAML
871877
added: v5.12.0
872878
changes:
879+
- version: REPLACEME
880+
pr-url: https://github.com/nodejs/node/pull/65003
881+
description: Added the `alignment` argument.
873882
- version: v20.0.0
874883
pr-url: https://github.com/nodejs/node/pull/45796
875884
description: Throw ERR_INVALID_ARG_TYPE or ERR_OUT_OF_RANGE instead of
@@ -881,6 +890,9 @@ changes:
881890
-->
882891

883892
*`size` {integer} The desired length of the new `Buffer`.
893+
*`alignment` {integer} If given, the memory backing the new `Buffer` will start
894+
at an address that is a multiple of `alignment`. Must be a power of two no
895+
larger than `2 ** 30`. See [Aligned allocations][].
884896
* Returns: {Buffer}
885897

886898
Allocates a new `Buffer` of `size` bytes. If `size` is larger than
@@ -5608,16 +5620,92 @@ While there are clear performance advantages to using
56085620
[`Buffer.allocUnsafe()`][], extra care _must_ be taken in order to avoid
56095621
introducing security vulnerabilities into an application.
56105622

5623+
### Aligned allocations
5624+
5625+
Some operating system interfaces require the memory they operate on to be
5626+
aligned, and on some hardware alignment is merely faster. The most common
5627+
example of the former is unbuffered ("direct") file I/O, which on Linux requires
5628+
the buffer address, the file offset and the transfer length to all be multiples
5629+
of the logical block size of the underlying device:
5630+
5631+
```mjs
5632+
import { open } from'node:fs/promises';
5633+
import { constants } from'node:fs';
5634+
import { Buffer } from'node:buffer';
5635+
5636+
constblockSize=4096;
5637+
5638+
// The buffer address must be block-aligned for O_DIRECT to accept it.
5639+
constbuf=Buffer.allocUnsafeSlow(blockSize, blockSize);
5640+
5641+
constfile=awaitopen('/dev/sda', constants.O_RDONLY|constants.O_DIRECT);
5642+
try {
5643+
awaitfile.read(buf, 0, blockSize, 0);
5644+
} finally {
5645+
awaitfile.close();
5646+
}
5647+
```
5648+
5649+
```cjs
5650+
constfs=require('node:fs');
5651+
const { Buffer } =require('node:buffer');
5652+
5653+
constblockSize=4096;
5654+
5655+
// The buffer address must be block-aligned for O_DIRECT to accept it.
5656+
constbuf=Buffer.allocUnsafeSlow(blockSize, blockSize);
5657+
5658+
constflags=fs.constants.O_RDONLY|fs.constants.O_DIRECT;
5659+
fs.open('/dev/sda', flags, (err, fd) => {
5660+
if (err) throw err;
5661+
fs.read(fd, buf, 0, blockSize, 0, (err) => {
5662+
fs.close(fd, () => {});
5663+
if (err) throw err;
5664+
});
5665+
});
5666+
```
5667+
5668+
Alignment can also be worth requesting purely for performance, even when no
5669+
interface demands it. Aligning a hot `Buffer` to the cache line size (64 bytes on
5670+
most contemporary CPUs) keeps it from straddling one more cache line than it
5671+
needs to, so that a small structure is fetched with one cache miss instead of
5672+
two, and page-aligned (4096 bytes) allocations similarly help interfaces that map
5673+
or pin memory. These are micro-optimizations: measure before reaching for them,
5674+
since the extra bytes are not free.
5675+
5676+
Because the address of a `Buffer`'s memory cannot be chosen directly, extra bytes
5677+
have to be allocated or skipped to reach an aligned address.
5678+
[`Buffer.allocUnsafeSlow()`][] over-allocates up to `alignment - 1` bytes and
5679+
positions the returned `Buffer` at the first suitably aligned byte within them.
5680+
[`Buffer.allocUnsafe()`][] instead pads its offset into the shared internal pool,
5681+
whose start is always aligned to 64 bytes, and only falls back to an allocation
5682+
of its own when `alignment` is larger than that. Either way,
5683+
[`buf.byteOffset`][] is usually not 0 and [`buf.buffer`][] is larger than `size`,
5684+
so code that reaches past the `Buffer` into its underlying `ArrayBuffer` must
5685+
take the offset into account, as it must for pooled `Buffer`s.
5686+
5687+
The alignment is a property of the returned `Buffer` and is preserved for its
5688+
whole lifetime, but it is not inherited by other views: [`buf.subarray`][],
5689+
[`buf.slice()`][] and `structuredClone()` may all produce unaligned `Buffer`s.
5690+
5691+
Alignment also does not survive being captured in a startup snapshot: memory does
5692+
not keep its address across serialization, so a `Buffer` allocated while
5693+
[`--build-snapshot`][] is in effect is not aligned in the deserialized process.
5694+
Allocate inside a [`v8.startupSnapshot.setDeserializeMainFunction()`][] callback,
5695+
or after startup, if the alignment has to hold at run time.
5696+
56115697
[ASCII]: https://en.wikipedia.org/wiki/ASCII
5698+
[Aligned allocations]: #aligned-allocations
56125699
[Base64]: https://en.wikipedia.org/wiki/Base64
56135700
[ISO-8859-1]: https://en.wikipedia.org/wiki/ISO-8859-1
56145701
[RFC 4648, Section 5]: https://tools.ietf.org/html/rfc4648#section-5
56155702
[UTF-16]: https://en.wikipedia.org/wiki/UTF-16
56165703
[UTF-8]: https://en.wikipedia.org/wiki/UTF-8
56175704
[WHATWG Encoding Standard]: https://encoding.spec.whatwg.org/
5705+
[`--build-snapshot`]: cli.md#--build-snapshot
56185706
[`Buffer.alloc()`]: #static-method-bufferallocsize-fill-encoding
5619-
[`Buffer.allocUnsafe()`]: #static-method-bufferallocunsafesize
5620-
[`Buffer.allocUnsafeSlow()`]: #static-method-bufferallocunsafeslowsize
5707+
[`Buffer.allocUnsafe()`]: #static-method-bufferallocunsafesize-alignment
5708+
[`Buffer.allocUnsafeSlow()`]: #static-method-bufferallocunsafeslowsize-alignment
56215709
[`Buffer.concat()`]: #static-method-bufferconcatlist-totallength
56225710
[`Buffer.copyBytesFrom()`]: #static-method-buffercopybytesfromview-offset-length
56235711
[`Buffer.from(array)`]: #static-method-bufferfromarray
@@ -5639,6 +5727,7 @@ introducing security vulnerabilities into an application.
56395727
[`TypedArray.prototype.subarray()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/subarray
56405728
[`blob.stream()`]: #blobstream
56415729
[`buf.buffer`]: #bufbuffer
5730+
[`buf.byteOffset`]: #bufbyteoffset
56425731
[`buf.compare()`]: #bufcomparetarget-targetstart-targetend-sourcestart-sourceend
56435732
[`buf.entries()`]: #bufentries
56445733
[`buf.fill()`]: #buffillvalue-offset-end-encoding
@@ -5653,6 +5742,7 @@ introducing security vulnerabilities into an application.
56535742
[`buffer.constants.MAX_STRING_LENGTH`]: #bufferconstantsmax_string_length
56545743
[`buffer.kMaxLength`]: #bufferkmaxlength
56555744
[`util.inspect()`]: util.md#utilinspectobject-options
5745+
[`v8.startupSnapshot.setDeserializeMainFunction()`]: v8.md#v8startupsnapshotsetdeserializemainfunctioncallback-data
56565746
[`v8::Uint8Array::kMaxLength`]: https://v8.github.io/api/head/classv8_1_1Uint8Array.html#a7677e3d0c9c92e4d40bef7212f5980c6
56575747
[base64url]: https://tools.ietf.org/html/rfc4648#section-5
56585748
[endianness]: https://en.wikipedia.org/wiki/Endianness

β€Ždoc/api/deprecations.mdβ€Ž

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4624,7 +4624,7 @@ will throw an error in a future version.
46244624
[`--pending-deprecation`]: cli.md#--pending-deprecation
46254625
[`--throw-deprecation`]: cli.md#--throw-deprecation
46264626
[`--unhandled-rejections`]: cli.md#--unhandled-rejectionsmode
4627-
[`Buffer.allocUnsafeSlow(size)`]: buffer.md#static-method-bufferallocunsafeslowsize
4627+
[`Buffer.allocUnsafeSlow(size)`]: buffer.md#static-method-bufferallocunsafeslowsize-alignment
46284628
[`Buffer.from(array)`]: buffer.md#static-method-bufferfromarray
46294629
[`Buffer.from(buffer)`]: buffer.md#static-method-bufferfrombuffer
46304630
[`Buffer.isBuffer()`]: buffer.md#static-method-bufferisbufferobj
@@ -4770,7 +4770,7 @@ will throw an error in a future version.
47704770
[`writable.writableLength`]: stream.md#writablewritablelength
47714771
[`zlib.bytesWritten`]: zlib.md#zlibbyteswritten
47724772
[alloc]: buffer.md#static-method-bufferallocsize-fill-encoding
4773-
[alloc_unsafe_size]: buffer.md#static-method-bufferallocunsafesize
4773+
[alloc_unsafe_size]: buffer.md#static-method-bufferallocunsafesize-alignment
47744774
[caveats of asynchronous customization hooks]: module.md#caveats-of-asynchronous-customization-hooks
47754775
[from_arraybuffer]: buffer.md#static-method-bufferfromarraybuffer-byteoffset-length
47764776
[from_string_encoding]: buffer.md#static-method-bufferfromstring-encoding

β€Ždoc/api/worker_threads.mdβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2234,7 +2234,7 @@ thread spawned will spawn another until the application crashes.
22342234
[`--max-old-space-size`]:cli.md#--max-old-space-sizesize-in-mib
22352235
[`--max-semi-space-size`]:cli.md#--max-semi-space-sizesize-in-mib
22362236
[`AsyncResource`]:async_hooks.md#class-asyncresource
2237-
[`Buffer.allocUnsafe()`]:buffer.md#static-method-bufferallocunsafesize
2237+
[`Buffer.allocUnsafe()`]:buffer.md#static-method-bufferallocunsafesize-alignment
22382238
[`ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST`]:errors.md#err_missing_message_port_in_transfer_list
22392239
[`ERR_WORKER_MESSAGING_ERRORED`]:errors.md#err_worker_messaging_errored
22402240
[`ERR_WORKER_MESSAGING_FAILED`]:errors.md#err_worker_messaging_failed

β€Žlib/buffer.jsβ€Ž

Lines changed: 77 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ const {
140140
markAsUntransferable,
141141
addBufferPrototypeMethods,
142142
createUnsafeBuffer,
143+
createUnsafeAlignedBuffer,
143144
asciiWrite,
144145
latin1Write,
145146
utf8Write,
@@ -171,13 +172,27 @@ const constants = ObjectDefineProperties({}, {
171172
},
172173
});
173174

175+
// The largest alignment accepted by `Buffer.allocUnsafeSlow()`. Any plausible
176+
// I/O alignment requirement (logical block size, memory page size, huge page
177+
// size) is well below this.
178+
constkMaxAlignment=2**30;
179+
180+
// Slices handed out of the pool are 8 byte aligned relative to the start of the
181+
// pool, so aligning the pool itself to a cache line keeps them from straddling
182+
// one more cache line than their size requires.
183+
constkPoolAlignment=64;
184+
174185
Buffer.poolSize=64*1024;
175-
letpoolSize,poolOffset,allocPool,allocBuffer;
186+
// `poolOffset` is relative to `poolBase`, which is where the pool starts inside
187+
// `allocPool`. The pool is over-allocated to be able to align it, so `poolBase`
188+
// is not necessarily 0.
189+
letpoolSize,poolOffset,poolBase,allocPool,allocBuffer;
176190

177191
functioncreatePool(){
178192
poolSize=Buffer.poolSize;
179-
allocBuffer=createUnsafeBuffer(poolSize);
193+
allocBuffer=createUnsafeAlignedBuffer(poolSize,kPoolAlignment);
180194
allocPool=allocBuffer.buffer;
195+
poolBase=TypedArrayPrototypeGetByteOffset(allocBuffer);
181196
markAsUntransferable(allocPool);
182197
poolOffset=0;
183198
}
@@ -444,40 +459,92 @@ Buffer.alloc = function alloc(size, fill, encoding) {
444459
/**
445460
* Equivalent to Buffer(num), by default creates a non-zero-filled Buffer
446461
* instance. If `--zero-fill-buffers` is set, will zero-fill the buffer.
462+
*
463+
* If `alignment` is given, the memory backing the returned buffer starts at an
464+
* address that is a multiple of `alignment`. See `Buffer.allocUnsafeSlow()`.
465+
* @param {number} size
466+
* @param {number} [alignment] A power of two, at most 2 ** 30
447467
* @returns {FastBuffer}
448468
*/
449-
Buffer.allocUnsafe=functionallocUnsafe(size){
469+
Buffer.allocUnsafe=functionallocUnsafe(size,alignment){
450470
validateNumber(size,'size',0,kMaxLength);
451-
returnallocate(size);
471+
if(alignment===undefined){
472+
returnallocate(size);
473+
}
474+
validateAlignment(size,alignment);
475+
returnallocateAligned(size,alignment);
452476
};
453477

454478
/**
455479
* By default creates a non-zero-filled Buffer instance that is not allocated
456480
* off the pre-initialized pool. If `--zero-fill-buffers` is set, will zero-fill
457481
* the buffer.
482+
*
483+
* If `alignment` is given, the memory backing the returned buffer starts at an
484+
* address that is a multiple of `alignment`, which is required by e.g. reads
485+
* and writes on file descriptors opened with `O_DIRECT`. Note that up to
486+
* `alignment - 1` extra bytes are allocated to satisfy the request, and that
487+
* the returned buffer's `byteOffset` is therefore usually non-zero.
458488
* @param {number} size
459-
* @returns {FastBuffer|undefined}
489+
* @param {number} [alignment] A power of two, at most 2 ** 30
490+
* @returns {FastBuffer}
460491
*/
461-
Buffer.allocUnsafeSlow=functionallocUnsafeSlow(size){
492+
Buffer.allocUnsafeSlow=functionallocUnsafeSlow(size,alignment){
462493
validateNumber(size,'size',0,kMaxLength);
463-
returncreateUnsafeBuffer(size);
494+
if(alignment===undefined){
495+
returncreateUnsafeBuffer(size);
496+
}
497+
validateAlignment(size,alignment);
498+
returncreateUnsafeAlignedBuffer(size,alignment);
464499
};
465500

501+
functionvalidateAlignment(size,alignment){
502+
validateInteger(alignment,'alignment',1,kMaxAlignment);
503+
if((alignment&(alignment-1))!==0){
504+
thrownewERR_INVALID_ARG_VALUE(
505+
'alignment',alignment,'must be a power of two');
506+
}
507+
// Satisfying the alignment costs up to `alignment - 1` extra bytes.
508+
if(size>kMaxLength-(alignment-1)){
509+
thrownewERR_OUT_OF_RANGE(
510+
'size',`<= ${kMaxLength-(alignment-1)}`,size);
511+
}
512+
}
513+
466514
functionallocate(size){
467515
if(size<=0){
468516
returnnewFastBuffer();
469517
}
470518
if(size<(Buffer.poolSize>>>1)){
471519
if(size>(poolSize-poolOffset))
472520
createPool();
473-
constb=newFastBuffer(allocPool,poolOffset,size);
521+
constb=newFastBuffer(allocPool,poolBase+poolOffset,size);
474522
poolOffset+=size;
475523
alignPool();
476524
returnb;
477525
}
478526
returncreateUnsafeBuffer(size);
479527
}
480528

529+
functionallocateAligned(size,alignment){
530+
if(size<=0){
531+
returnnewFastBuffer();
532+
}
533+
// The pool starts at a `kPoolAlignment` aligned address, so any alignment up
534+
// to that can be satisfied by padding the offset into the pool. Stricter
535+
// alignments need an allocation of their own.
536+
if(alignment>kPoolAlignment||size>=(Buffer.poolSize>>>1)){
537+
returncreateUnsafeAlignedBuffer(size,alignment);
538+
}
539+
poolOffset=(poolOffset+alignment-1)&~(alignment-1);
540+
if(size>(poolSize-poolOffset))
541+
createPool();
542+
constb=newFastBuffer(allocPool,poolBase+poolOffset,size);
543+
poolOffset+=size;
544+
alignPool();
545+
returnb;
546+
}
547+
481548
functionfromStringFast(string,ops){
482549
constmaxLength=Buffer.poolSize>>>1;
483550

@@ -498,7 +565,7 @@ function fromStringFast(string, ops) {
498565
createPool();
499566

500567
constactual=ops.write(allocBuffer,string,poolOffset,length);
501-
constb=newFastBuffer(allocPool,poolOffset,actual);
568+
constb=newFastBuffer(allocPool,poolBase+poolOffset,actual);
502569

503570
poolOffset+=actual;
504571
alignPool();
@@ -560,7 +627,7 @@ function fromArrayLike(obj) {
560627
if(length<(Buffer.poolSize>>>1)){
561628
if(length>(poolSize-poolOffset))
562629
createPool();
563-
constb=newFastBuffer(allocPool,poolOffset,length);
630+
constb=newFastBuffer(allocPool,poolBase+poolOffset,length);
564631
TypedArrayPrototypeSet(b,obj,0);
565632
poolOffset+=length;
566633
alignPool();

β€Žlib/internal/buffer.jsβ€Ž

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const {
3333
hexWrite,
3434
ucs2Write,
3535
utf8WriteStatic,
36+
arrayBufferAlignedOffset,
3637
createUnsafeArrayBuffer,
3738
setDetachKey,
3839
}=internalBinding('buffer');
@@ -1104,12 +1105,28 @@ function createUnsafeBuffer(size) {
11041105
returnnewFastBuffer(createUnsafeArrayBuffer(size));
11051106
}
11061107

1108+
// Returns an uninitialized buffer of `size` bytes whose first byte is located at
1109+
// a memory address that is a multiple of `alignment`. `alignment` must be a
1110+
// power of two, and `size + alignment - 1` must not exceed the maximum buffer
1111+
// length. Since the address of a backing store cannot be chosen, `alignment - 1`
1112+
// extra bytes are allocated and skipped, which leaves the returned buffer with a
1113+
// non-zero `byteOffset` into a larger ArrayBuffer.
1114+
functioncreateUnsafeAlignedBuffer(size,alignment){
1115+
if(size===0){
1116+
returnnewFastBuffer();
1117+
}
1118+
1119+
constab=createUnsafeArrayBuffer(size+alignment-1);
1120+
returnnewFastBuffer(ab,arrayBufferAlignedOffset(ab,alignment),size);
1121+
}
1122+
11071123
module.exports={
11081124
FastBuffer,
11091125
addBufferPrototypeMethods,
11101126
markAsUntransferable,
11111127
isMarkedAsUntransferable,
11121128
createUnsafeBuffer,
1129+
createUnsafeAlignedBuffer,
11131130
readUInt16BE,
11141131
readUInt32BE,
11151132
asciiWrite,

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Commit 615273d

Browse files
ronagclaude
authored andcommitted
buffer: support aligned allocations
Add an optional `alignment` argument to `Buffer.allocUnsafe()` and `Buffer.allocUnsafeSlow()`, which guarantees that the memory backing the returned buffer starts at an address that is a multiple of `alignment`. Some operating system interfaces refuse to work with unaligned memory. The motivating case is unbuffered ("direct") file I/O: a read or write on a descriptor opened with `O_DIRECT` fails with `EINVAL` unless the buffer address is a multiple of the logical block size of the underlying device. Until now there was no way to obtain such a buffer from JS, since the address of a backing store can neither be observed nor chosen. Alignment is also worth having purely for performance, for instance to keep a hot buffer from straddling one more cache line than its size requires. V8 does not allow picking the address of a backing store, so alignment is instead achieved by over-allocating `alignment - 1` bytes and positioning the buffer at the first suitably aligned byte within them. The new `arrayBufferAlignedOffset()` binding computes that offset. Addresses are not stable across snapshot serialization, so the binding reports no padding while a snapshot is being built. Otherwise the snapshot would capture where this particular process happened to allocate and stop being reproducible. Buffers restored from a snapshot are consequently not aligned; the pool works around this by recreating itself in a deserialize callback. The `Buffer.allocUnsafe()` pool is now aligned to a cache line itself, which lets pooled allocations satisfy any alignment up to 64 bytes by padding their offset into the pool rather than allocating separately. Assisted-by: Claude/Opus 5 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Robert Nagy <ronagy@icloud.com> PR-URL: #65003 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
1 parent 49fb028 commit 615273d

7 files changed

Lines changed: 415 additions & 35 deletions

File tree

β€Ždoc/api/buffer.mdβ€Ž

Lines changed: 94 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -791,11 +791,14 @@ data that might not have been allocated for `Buffer`s.
791791

792792
A `TypeError` will be thrown if `size` is not a number.
793793

794-
### Static method: `Buffer.allocUnsafe(size)`
794+
### Static method: `Buffer.allocUnsafe(size[, alignment])`
795795

796796
<!-- YAML
797797
added: v5.10.0
798798
changes:
799+
- version: REPLACEME
800+
pr-url: https://github.com/nodejs/node/pull/65003
801+
description: Added the `alignment` argument.
799802
- version: v20.0.0
800803
pr-url: https://github.com/nodejs/node/pull/45796
801804
description: Throw ERR_INVALID_ARG_TYPE or ERR_OUT_OF_RANGE instead of
@@ -810,6 +813,9 @@ changes:
810813
-->
811814

812815
*`size` {integer} The desired length of the new `Buffer`.
816+
*`alignment` {integer} If given, the memory backing the new `Buffer` will start
817+
at an address that is a multiple of `alignment`. Must be a power of two no
818+
larger than `2 ** 30`. See [Aligned allocations][].
813819
* Returns: {Buffer}
814820

815821
Allocates a new `Buffer` of `size` bytes. If `size` is larger than
@@ -865,11 +871,14 @@ pool, while `Buffer.allocUnsafe(size).fill(fill)` _will_ use the internal
865871
difference is subtle but can be important when an application requires the
866872
additional performance that [`Buffer.allocUnsafe()`][] provides.
867873

868-
### Static method: `Buffer.allocUnsafeSlow(size)`
874+
### Static method: `Buffer.allocUnsafeSlow(size[, alignment])`
869875

870876
<!-- YAML
871877
added: v5.12.0
872878
changes:
879+
- version: REPLACEME
880+
pr-url: https://github.com/nodejs/node/pull/65003
881+
description: Added the `alignment` argument.
873882
- version: v20.0.0
874883
pr-url: https://github.com/nodejs/node/pull/45796
875884
description: Throw ERR_INVALID_ARG_TYPE or ERR_OUT_OF_RANGE instead of
@@ -881,6 +890,9 @@ changes:
881890
-->
882891

883892
*`size` {integer} The desired length of the new `Buffer`.
893+
*`alignment` {integer} If given, the memory backing the new `Buffer` will start
894+
at an address that is a multiple of `alignment`. Must be a power of two no
895+
larger than `2 ** 30`. See [Aligned allocations][].
884896
* Returns: {Buffer}
885897

886898
Allocates a new `Buffer` of `size` bytes. If `size` is larger than
@@ -5608,16 +5620,92 @@ While there are clear performance advantages to using
56085620
[`Buffer.allocUnsafe()`][], extra care _must_ be taken in order to avoid
56095621
introducing security vulnerabilities into an application.
56105622

5623+
### Aligned allocations
5624+
5625+
Some operating system interfaces require the memory they operate on to be
5626+
aligned, and on some hardware alignment is merely faster. The most common
5627+
example of the former is unbuffered ("direct") file I/O, which on Linux requires
5628+
the buffer address, the file offset and the transfer length to all be multiples
5629+
of the logical block size of the underlying device:
5630+
5631+
```mjs
5632+
import { open } from'node:fs/promises';
5633+
import { constants } from'node:fs';
5634+
import { Buffer } from'node:buffer';
5635+
5636+
constblockSize=4096;
5637+
5638+
// The buffer address must be block-aligned for O_DIRECT to accept it.
5639+
constbuf=Buffer.allocUnsafeSlow(blockSize, blockSize);
5640+
5641+
constfile=awaitopen('/dev/sda', constants.O_RDONLY|constants.O_DIRECT);
5642+
try {
5643+
awaitfile.read(buf, 0, blockSize, 0);
5644+
} finally {
5645+
awaitfile.close();
5646+
}
5647+
```
5648+
5649+
```cjs
5650+
constfs=require('node:fs');
5651+
const { Buffer } =require('node:buffer');
5652+
5653+
constblockSize=4096;
5654+
5655+
// The buffer address must be block-aligned for O_DIRECT to accept it.
5656+
constbuf=Buffer.allocUnsafeSlow(blockSize, blockSize);
5657+
5658+
constflags=fs.constants.O_RDONLY|fs.constants.O_DIRECT;
5659+
fs.open('/dev/sda', flags, (err, fd) => {
5660+
if (err) throw err;
5661+
fs.read(fd, buf, 0, blockSize, 0, (err) => {
5662+
fs.close(fd, () => {});
5663+
if (err) throw err;
5664+
});
5665+
});
5666+
```
5667+
5668+
Alignment can also be worth requesting purely for performance, even when no
5669+
interface demands it. Aligning a hot `Buffer` to the cache line size (64 bytes on
5670+
most contemporary CPUs) keeps it from straddling one more cache line than it
5671+
needs to, so that a small structure is fetched with one cache miss instead of
5672+
two, and page-aligned (4096 bytes) allocations similarly help interfaces that map
5673+
or pin memory. These are micro-optimizations: measure before reaching for them,
5674+
since the extra bytes are not free.
5675+
5676+
Because the address of a `Buffer`'s memory cannot be chosen directly, extra bytes
5677+
have to be allocated or skipped to reach an aligned address.
5678+
[`Buffer.allocUnsafeSlow()`][] over-allocates up to `alignment - 1` bytes and
5679+
positions the returned `Buffer` at the first suitably aligned byte within them.
5680+
[`Buffer.allocUnsafe()`][] instead pads its offset into the shared internal pool,
5681+
whose start is always aligned to 64 bytes, and only falls back to an allocation
5682+
of its own when `alignment` is larger than that. Either way,
5683+
[`buf.byteOffset`][] is usually not 0 and [`buf.buffer`][] is larger than `size`,
5684+
so code that reaches past the `Buffer` into its underlying `ArrayBuffer` must
5685+
take the offset into account, as it must for pooled `Buffer`s.
5686+
5687+
The alignment is a property of the returned `Buffer` and is preserved for its
5688+
whole lifetime, but it is not inherited by other views: [`buf.subarray`][],
5689+
[`buf.slice()`][] and `structuredClone()` may all produce unaligned `Buffer`s.
5690+
5691+
Alignment also does not survive being captured in a startup snapshot: memory does
5692+
not keep its address across serialization, so a `Buffer` allocated while
5693+
[`--build-snapshot`][] is in effect is not aligned in the deserialized process.
5694+
Allocate inside a [`v8.startupSnapshot.setDeserializeMainFunction()`][] callback,
5695+
or after startup, if the alignment has to hold at run time.
5696+
56115697
[ASCII]: https://en.wikipedia.org/wiki/ASCII
5698+
[Aligned allocations]: #aligned-allocations
56125699
[Base64]: https://en.wikipedia.org/wiki/Base64
56135700
[ISO-8859-1]: https://en.wikipedia.org/wiki/ISO-8859-1
56145701
[RFC 4648, Section 5]: https://tools.ietf.org/html/rfc4648#section-5
56155702
[UTF-16]: https://en.wikipedia.org/wiki/UTF-16
56165703
[UTF-8]: https://en.wikipedia.org/wiki/UTF-8
56175704
[WHATWG Encoding Standard]: https://encoding.spec.whatwg.org/
5705+
[`--build-snapshot`]: cli.md#--build-snapshot
56185706
[`Buffer.alloc()`]: #static-method-bufferallocsize-fill-encoding
5619-
[`Buffer.allocUnsafe()`]: #static-method-bufferallocunsafesize
5620-
[`Buffer.allocUnsafeSlow()`]: #static-method-bufferallocunsafeslowsize
5707+
[`Buffer.allocUnsafe()`]: #static-method-bufferallocunsafesize-alignment
5708+
[`Buffer.allocUnsafeSlow()`]: #static-method-bufferallocunsafeslowsize-alignment
56215709
[`Buffer.concat()`]: #static-method-bufferconcatlist-totallength
56225710
[`Buffer.copyBytesFrom()`]: #static-method-buffercopybytesfromview-offset-length
56235711
[`Buffer.from(array)`]: #static-method-bufferfromarray
@@ -5639,6 +5727,7 @@ introducing security vulnerabilities into an application.
56395727
[`TypedArray.prototype.subarray()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/subarray
56405728
[`blob.stream()`]: #blobstream
56415729
[`buf.buffer`]: #bufbuffer
5730+
[`buf.byteOffset`]: #bufbyteoffset
56425731
[`buf.compare()`]: #bufcomparetarget-targetstart-targetend-sourcestart-sourceend
56435732
[`buf.entries()`]: #bufentries
56445733
[`buf.fill()`]: #buffillvalue-offset-end-encoding
@@ -5653,6 +5742,7 @@ introducing security vulnerabilities into an application.
56535742
[`buffer.constants.MAX_STRING_LENGTH`]: #bufferconstantsmax_string_length
56545743
[`buffer.kMaxLength`]: #bufferkmaxlength
56555744
[`util.inspect()`]: util.md#utilinspectobject-options
5745+
[`v8.startupSnapshot.setDeserializeMainFunction()`]: v8.md#v8startupsnapshotsetdeserializemainfunctioncallback-data
56565746
[`v8::Uint8Array::kMaxLength`]: https://v8.github.io/api/head/classv8_1_1Uint8Array.html#a7677e3d0c9c92e4d40bef7212f5980c6
56575747
[base64url]: https://tools.ietf.org/html/rfc4648#section-5
56585748
[endianness]: https://en.wikipedia.org/wiki/Endianness

β€Ždoc/api/deprecations.mdβ€Ž

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4624,7 +4624,7 @@ will throw an error in a future version.
46244624
[`--pending-deprecation`]: cli.md#--pending-deprecation
46254625
[`--throw-deprecation`]: cli.md#--throw-deprecation
46264626
[`--unhandled-rejections`]: cli.md#--unhandled-rejectionsmode
4627-
[`Buffer.allocUnsafeSlow(size)`]: buffer.md#static-method-bufferallocunsafeslowsize
4627+
[`Buffer.allocUnsafeSlow(size)`]: buffer.md#static-method-bufferallocunsafeslowsize-alignment
46284628
[`Buffer.from(array)`]: buffer.md#static-method-bufferfromarray
46294629
[`Buffer.from(buffer)`]: buffer.md#static-method-bufferfrombuffer
46304630
[`Buffer.isBuffer()`]: buffer.md#static-method-bufferisbufferobj
@@ -4770,7 +4770,7 @@ will throw an error in a future version.
47704770
[`writable.writableLength`]: stream.md#writablewritablelength
47714771
[`zlib.bytesWritten`]: zlib.md#zlibbyteswritten
47724772
[alloc]: buffer.md#static-method-bufferallocsize-fill-encoding
4773-
[alloc_unsafe_size]: buffer.md#static-method-bufferallocunsafesize
4773+
[alloc_unsafe_size]: buffer.md#static-method-bufferallocunsafesize-alignment
47744774
[caveats of asynchronous customization hooks]: module.md#caveats-of-asynchronous-customization-hooks
47754775
[from_arraybuffer]: buffer.md#static-method-bufferfromarraybuffer-byteoffset-length
47764776
[from_string_encoding]: buffer.md#static-method-bufferfromstring-encoding

β€Ždoc/api/worker_threads.mdβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2234,7 +2234,7 @@ thread spawned will spawn another until the application crashes.
22342234
[`--max-old-space-size`]:cli.md#--max-old-space-sizesize-in-mib
22352235
[`--max-semi-space-size`]:cli.md#--max-semi-space-sizesize-in-mib
22362236
[`AsyncResource`]:async_hooks.md#class-asyncresource
2237-
[`Buffer.allocUnsafe()`]:buffer.md#static-method-bufferallocunsafesize
2237+
[`Buffer.allocUnsafe()`]:buffer.md#static-method-bufferallocunsafesize-alignment
22382238
[`ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST`]:errors.md#err_missing_message_port_in_transfer_list
22392239
[`ERR_WORKER_MESSAGING_ERRORED`]:errors.md#err_worker_messaging_errored
22402240
[`ERR_WORKER_MESSAGING_FAILED`]:errors.md#err_worker_messaging_failed

β€Žlib/buffer.jsβ€Ž

Lines changed: 77 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ const {
140140
markAsUntransferable,
141141
addBufferPrototypeMethods,
142142
createUnsafeBuffer,
143+
createUnsafeAlignedBuffer,
143144
asciiWrite,
144145
latin1Write,
145146
utf8Write,
@@ -171,13 +172,27 @@ const constants = ObjectDefineProperties({}, {
171172
},
172173
});
173174

175+
// The largest alignment accepted by `Buffer.allocUnsafeSlow()`. Any plausible
176+
// I/O alignment requirement (logical block size, memory page size, huge page
177+
// size) is well below this.
178+
constkMaxAlignment=2**30;
179+
180+
// Slices handed out of the pool are 8 byte aligned relative to the start of the
181+
// pool, so aligning the pool itself to a cache line keeps them from straddling
182+
// one more cache line than their size requires.
183+
constkPoolAlignment=64;
184+
174185
Buffer.poolSize=64*1024;
175-
letpoolSize,poolOffset,allocPool,allocBuffer;
186+
// `poolOffset` is relative to `poolBase`, which is where the pool starts inside
187+
// `allocPool`. The pool is over-allocated to be able to align it, so `poolBase`
188+
// is not necessarily 0.
189+
letpoolSize,poolOffset,poolBase,allocPool,allocBuffer;
176190

177191
functioncreatePool(){
178192
poolSize=Buffer.poolSize;
179-
allocBuffer=createUnsafeBuffer(poolSize);
193+
allocBuffer=createUnsafeAlignedBuffer(poolSize,kPoolAlignment);
180194
allocPool=allocBuffer.buffer;
195+
poolBase=TypedArrayPrototypeGetByteOffset(allocBuffer);
181196
markAsUntransferable(allocPool);
182197
poolOffset=0;
183198
}
@@ -444,40 +459,92 @@ Buffer.alloc = function alloc(size, fill, encoding) {
444459
/**
445460
* Equivalent to Buffer(num), by default creates a non-zero-filled Buffer
446461
* instance. If `--zero-fill-buffers` is set, will zero-fill the buffer.
462+
*
463+
* If `alignment` is given, the memory backing the returned buffer starts at an
464+
* address that is a multiple of `alignment`. See `Buffer.allocUnsafeSlow()`.
465+
* @param {number} size
466+
* @param {number} [alignment] A power of two, at most 2 ** 30
447467
* @returns {FastBuffer}
448468
*/
449-
Buffer.allocUnsafe=functionallocUnsafe(size){
469+
Buffer.allocUnsafe=functionallocUnsafe(size,alignment){
450470
validateNumber(size,'size',0,kMaxLength);
451-
returnallocate(size);
471+
if(alignment===undefined){
472+
returnallocate(size);
473+
}
474+
validateAlignment(size,alignment);
475+
returnallocateAligned(size,alignment);
452476
};
453477

454478
/**
455479
* By default creates a non-zero-filled Buffer instance that is not allocated
456480
* off the pre-initialized pool. If `--zero-fill-buffers` is set, will zero-fill
457481
* the buffer.
482+
*
483+
* If `alignment` is given, the memory backing the returned buffer starts at an
484+
* address that is a multiple of `alignment`, which is required by e.g. reads
485+
* and writes on file descriptors opened with `O_DIRECT`. Note that up to
486+
* `alignment - 1` extra bytes are allocated to satisfy the request, and that
487+
* the returned buffer's `byteOffset` is therefore usually non-zero.
458488
* @param {number} size
459-
* @returns {FastBuffer|undefined}
489+
* @param {number} [alignment] A power of two, at most 2 ** 30
490+
* @returns {FastBuffer}
460491
*/
461-
Buffer.allocUnsafeSlow=functionallocUnsafeSlow(size){
492+
Buffer.allocUnsafeSlow=functionallocUnsafeSlow(size,alignment){
462493
validateNumber(size,'size',0,kMaxLength);
463-
returncreateUnsafeBuffer(size);
494+
if(alignment===undefined){
495+
returncreateUnsafeBuffer(size);
496+
}
497+
validateAlignment(size,alignment);
498+
returncreateUnsafeAlignedBuffer(size,alignment);
464499
};
465500

501+
functionvalidateAlignment(size,alignment){
502+
validateInteger(alignment,'alignment',1,kMaxAlignment);
503+
if((alignment&(alignment-1))!==0){
504+
thrownewERR_INVALID_ARG_VALUE(
505+
'alignment',alignment,'must be a power of two');
506+
}
507+
// Satisfying the alignment costs up to `alignment - 1` extra bytes.
508+
if(size>kMaxLength-(alignment-1)){
509+
thrownewERR_OUT_OF_RANGE(
510+
'size',`<= ${kMaxLength-(alignment-1)}`,size);
511+
}
512+
}
513+
466514
functionallocate(size){
467515
if(size<=0){
468516
returnnewFastBuffer();
469517
}
470518
if(size<(Buffer.poolSize>>>1)){
471519
if(size>(poolSize-poolOffset))
472520
createPool();
473-
constb=newFastBuffer(allocPool,poolOffset,size);
521+
constb=newFastBuffer(allocPool,poolBase+poolOffset,size);
474522
poolOffset+=size;
475523
alignPool();
476524
returnb;
477525
}
478526
returncreateUnsafeBuffer(size);
479527
}
480528

529+
functionallocateAligned(size,alignment){
530+
if(size<=0){
531+
returnnewFastBuffer();
532+
}
533+
// The pool starts at a `kPoolAlignment` aligned address, so any alignment up
534+
// to that can be satisfied by padding the offset into the pool. Stricter
535+
// alignments need an allocation of their own.
536+
if(alignment>kPoolAlignment||size>=(Buffer.poolSize>>>1)){
537+
returncreateUnsafeAlignedBuffer(size,alignment);
538+
}
539+
poolOffset=(poolOffset+alignment-1)&~(alignment-1);
540+
if(size>(poolSize-poolOffset))
541+
createPool();
542+
constb=newFastBuffer(allocPool,poolBase+poolOffset,size);
543+
poolOffset+=size;
544+
alignPool();
545+
returnb;
546+
}
547+
481548
functionfromStringFast(string,ops){
482549
constmaxLength=Buffer.poolSize>>>1;
483550

@@ -498,7 +565,7 @@ function fromStringFast(string, ops) {
498565
createPool();
499566

500567
constactual=ops.write(allocBuffer,string,poolOffset,length);
501-
constb=newFastBuffer(allocPool,poolOffset,actual);
568+
constb=newFastBuffer(allocPool,poolBase+poolOffset,actual);
502569

503570
poolOffset+=actual;
504571
alignPool();
@@ -560,7 +627,7 @@ function fromArrayLike(obj) {
560627
if(length<(Buffer.poolSize>>>1)){
561628
if(length>(poolSize-poolOffset))
562629
createPool();
563-
constb=newFastBuffer(allocPool,poolOffset,length);
630+
constb=newFastBuffer(allocPool,poolBase+poolOffset,length);
564631
TypedArrayPrototypeSet(b,obj,0);
565632
poolOffset+=length;
566633
alignPool();

β€Žlib/internal/buffer.jsβ€Ž

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const {
3333
hexWrite,
3434
ucs2Write,
3535
utf8WriteStatic,
36+
arrayBufferAlignedOffset,
3637
createUnsafeArrayBuffer,
3738
setDetachKey,
3839
}=internalBinding('buffer');
@@ -1104,12 +1105,28 @@ function createUnsafeBuffer(size) {
11041105
returnnewFastBuffer(createUnsafeArrayBuffer(size));
11051106
}
11061107

1108+
// Returns an uninitialized buffer of `size` bytes whose first byte is located at
1109+
// a memory address that is a multiple of `alignment`. `alignment` must be a
1110+
// power of two, and `size + alignment - 1` must not exceed the maximum buffer
1111+
// length. Since the address of a backing store cannot be chosen, `alignment - 1`
1112+
// extra bytes are allocated and skipped, which leaves the returned buffer with a
1113+
// non-zero `byteOffset` into a larger ArrayBuffer.
1114+
functioncreateUnsafeAlignedBuffer(size,alignment){
1115+
if(size===0){
1116+
returnnewFastBuffer();
1117+
}
1118+
1119+
constab=createUnsafeArrayBuffer(size+alignment-1);
1120+
returnnewFastBuffer(ab,arrayBufferAlignedOffset(ab,alignment),size);
1121+
}
1122+
11071123
module.exports={
11081124
FastBuffer,
11091125
addBufferPrototypeMethods,
11101126
markAsUntransferable,
11111127
isMarkedAsUntransferable,
11121128
createUnsafeBuffer,
1129+
createUnsafeAlignedBuffer,
11131130
readUInt16BE,
11141131
readUInt32BE,
11151132
asciiWrite,

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit 615273d

Browse files
ronagclaude
authored andcommitted
buffer: support aligned allocations
Add an optional `alignment` argument to `Buffer.allocUnsafe()` and `Buffer.allocUnsafeSlow()`, which guarantees that the memory backing the returned buffer starts at an address that is a multiple of `alignment`. Some operating system interfaces refuse to work with unaligned memory. The motivating case is unbuffered ("direct") file I/O: a read or write on a descriptor opened with `O_DIRECT` fails with `EINVAL` unless the buffer address is a multiple of the logical block size of the underlying device. Until now there was no way to obtain such a buffer from JS, since the address of a backing store can neither be observed nor chosen. Alignment is also worth having purely for performance, for instance to keep a hot buffer from straddling one more cache line than its size requires. V8 does not allow picking the address of a backing store, so alignment is instead achieved by over-allocating `alignment - 1` bytes and positioning the buffer at the first suitably aligned byte within them. The new `arrayBufferAlignedOffset()` binding computes that offset. Addresses are not stable across snapshot serialization, so the binding reports no padding while a snapshot is being built. Otherwise the snapshot would capture where this particular process happened to allocate and stop being reproducible. Buffers restored from a snapshot are consequently not aligned; the pool works around this by recreating itself in a deserialize callback. The `Buffer.allocUnsafe()` pool is now aligned to a cache line itself, which lets pooled allocations satisfy any alignment up to 64 bytes by padding their offset into the pool rather than allocating separately. Assisted-by: Claude/Opus 5 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Robert Nagy <ronagy@icloud.com> PR-URL: #65003 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
1 parent 49fb028 commit 615273d

7 files changed

Lines changed: 415 additions & 35 deletions

File tree

β€Ždoc/api/buffer.mdβ€Ž

Lines changed: 94 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -791,11 +791,14 @@ data that might not have been allocated for `Buffer`s.
791791

792792
A `TypeError` will be thrown if `size` is not a number.
793793

794-
### Static method: `Buffer.allocUnsafe(size)`
794+
### Static method: `Buffer.allocUnsafe(size[, alignment])`
795795

796796
<!-- YAML
797797
added: v5.10.0
798798
changes:
799+
- version: REPLACEME
800+
pr-url: https://github.com/nodejs/node/pull/65003
801+
description: Added the `alignment` argument.
799802
- version: v20.0.0
800803
pr-url: https://github.com/nodejs/node/pull/45796
801804
description: Throw ERR_INVALID_ARG_TYPE or ERR_OUT_OF_RANGE instead of
@@ -810,6 +813,9 @@ changes:
810813
-->
811814

812815
*`size` {integer} The desired length of the new `Buffer`.
816+
*`alignment` {integer} If given, the memory backing the new `Buffer` will start
817+
at an address that is a multiple of `alignment`. Must be a power of two no
818+
larger than `2 ** 30`. See [Aligned allocations][].
813819
* Returns: {Buffer}
814820

815821
Allocates a new `Buffer` of `size` bytes. If `size` is larger than
@@ -865,11 +871,14 @@ pool, while `Buffer.allocUnsafe(size).fill(fill)` _will_ use the internal
865871
difference is subtle but can be important when an application requires the
866872
additional performance that [`Buffer.allocUnsafe()`][] provides.
867873

868-
### Static method: `Buffer.allocUnsafeSlow(size)`
874+
### Static method: `Buffer.allocUnsafeSlow(size[, alignment])`
869875

870876
<!-- YAML
871877
added: v5.12.0
872878
changes:
879+
- version: REPLACEME
880+
pr-url: https://github.com/nodejs/node/pull/65003
881+
description: Added the `alignment` argument.
873882
- version: v20.0.0
874883
pr-url: https://github.com/nodejs/node/pull/45796
875884
description: Throw ERR_INVALID_ARG_TYPE or ERR_OUT_OF_RANGE instead of
@@ -881,6 +890,9 @@ changes:
881890
-->
882891

883892
*`size` {integer} The desired length of the new `Buffer`.
893+
*`alignment` {integer} If given, the memory backing the new `Buffer` will start
894+
at an address that is a multiple of `alignment`. Must be a power of two no
895+
larger than `2 ** 30`. See [Aligned allocations][].
884896
* Returns: {Buffer}
885897

886898
Allocates a new `Buffer` of `size` bytes. If `size` is larger than
@@ -5608,16 +5620,92 @@ While there are clear performance advantages to using
56085620
[`Buffer.allocUnsafe()`][], extra care _must_ be taken in order to avoid
56095621
introducing security vulnerabilities into an application.
56105622

5623+
### Aligned allocations
5624+
5625+
Some operating system interfaces require the memory they operate on to be
5626+
aligned, and on some hardware alignment is merely faster. The most common
5627+
example of the former is unbuffered ("direct") file I/O, which on Linux requires
5628+
the buffer address, the file offset and the transfer length to all be multiples
5629+
of the logical block size of the underlying device:
5630+
5631+
```mjs
5632+
import { open } from'node:fs/promises';
5633+
import { constants } from'node:fs';
5634+
import { Buffer } from'node:buffer';
5635+
5636+
constblockSize=4096;
5637+
5638+
// The buffer address must be block-aligned for O_DIRECT to accept it.
5639+
constbuf=Buffer.allocUnsafeSlow(blockSize, blockSize);
5640+
5641+
constfile=awaitopen('/dev/sda', constants.O_RDONLY|constants.O_DIRECT);
5642+
try {
5643+
awaitfile.read(buf, 0, blockSize, 0);
5644+
} finally {
5645+
awaitfile.close();
5646+
}
5647+
```
5648+
5649+
```cjs
5650+
constfs=require('node:fs');
5651+
const { Buffer } =require('node:buffer');
5652+
5653+
constblockSize=4096;
5654+
5655+
// The buffer address must be block-aligned for O_DIRECT to accept it.
5656+
constbuf=Buffer.allocUnsafeSlow(blockSize, blockSize);
5657+
5658+
constflags=fs.constants.O_RDONLY|fs.constants.O_DIRECT;
5659+
fs.open('/dev/sda', flags, (err, fd) => {
5660+
if (err) throw err;
5661+
fs.read(fd, buf, 0, blockSize, 0, (err) => {
5662+
fs.close(fd, () => {});
5663+
if (err) throw err;
5664+
});
5665+
});
5666+
```
5667+
5668+
Alignment can also be worth requesting purely for performance, even when no
5669+
interface demands it. Aligning a hot `Buffer` to the cache line size (64 bytes on
5670+
most contemporary CPUs) keeps it from straddling one more cache line than it
5671+
needs to, so that a small structure is fetched with one cache miss instead of
5672+
two, and page-aligned (4096 bytes) allocations similarly help interfaces that map
5673+
or pin memory. These are micro-optimizations: measure before reaching for them,
5674+
since the extra bytes are not free.
5675+
5676+
Because the address of a `Buffer`'s memory cannot be chosen directly, extra bytes
5677+
have to be allocated or skipped to reach an aligned address.
5678+
[`Buffer.allocUnsafeSlow()`][] over-allocates up to `alignment - 1` bytes and
5679+
positions the returned `Buffer` at the first suitably aligned byte within them.
5680+
[`Buffer.allocUnsafe()`][] instead pads its offset into the shared internal pool,
5681+
whose start is always aligned to 64 bytes, and only falls back to an allocation
5682+
of its own when `alignment` is larger than that. Either way,
5683+
[`buf.byteOffset`][] is usually not 0 and [`buf.buffer`][] is larger than `size`,
5684+
so code that reaches past the `Buffer` into its underlying `ArrayBuffer` must
5685+
take the offset into account, as it must for pooled `Buffer`s.
5686+
5687+
The alignment is a property of the returned `Buffer` and is preserved for its
5688+
whole lifetime, but it is not inherited by other views: [`buf.subarray`][],
5689+
[`buf.slice()`][] and `structuredClone()` may all produce unaligned `Buffer`s.
5690+
5691+
Alignment also does not survive being captured in a startup snapshot: memory does
5692+
not keep its address across serialization, so a `Buffer` allocated while
5693+
[`--build-snapshot`][] is in effect is not aligned in the deserialized process.
5694+
Allocate inside a [`v8.startupSnapshot.setDeserializeMainFunction()`][] callback,
5695+
or after startup, if the alignment has to hold at run time.
5696+
56115697
[ASCII]: https://en.wikipedia.org/wiki/ASCII
5698+
[Aligned allocations]: #aligned-allocations
56125699
[Base64]: https://en.wikipedia.org/wiki/Base64
56135700
[ISO-8859-1]: https://en.wikipedia.org/wiki/ISO-8859-1
56145701
[RFC 4648, Section 5]: https://tools.ietf.org/html/rfc4648#section-5
56155702
[UTF-16]: https://en.wikipedia.org/wiki/UTF-16
56165703
[UTF-8]: https://en.wikipedia.org/wiki/UTF-8
56175704
[WHATWG Encoding Standard]: https://encoding.spec.whatwg.org/
5705+
[`--build-snapshot`]: cli.md#--build-snapshot
56185706
[`Buffer.alloc()`]: #static-method-bufferallocsize-fill-encoding
5619-
[`Buffer.allocUnsafe()`]: #static-method-bufferallocunsafesize
5620-
[`Buffer.allocUnsafeSlow()`]: #static-method-bufferallocunsafeslowsize
5707+
[`Buffer.allocUnsafe()`]: #static-method-bufferallocunsafesize-alignment
5708+
[`Buffer.allocUnsafeSlow()`]: #static-method-bufferallocunsafeslowsize-alignment
56215709
[`Buffer.concat()`]: #static-method-bufferconcatlist-totallength
56225710
[`Buffer.copyBytesFrom()`]: #static-method-buffercopybytesfromview-offset-length
56235711
[`Buffer.from(array)`]: #static-method-bufferfromarray
@@ -5639,6 +5727,7 @@ introducing security vulnerabilities into an application.
56395727
[`TypedArray.prototype.subarray()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/subarray
56405728
[`blob.stream()`]: #blobstream
56415729
[`buf.buffer`]: #bufbuffer
5730+
[`buf.byteOffset`]: #bufbyteoffset
56425731
[`buf.compare()`]: #bufcomparetarget-targetstart-targetend-sourcestart-sourceend
56435732
[`buf.entries()`]: #bufentries
56445733
[`buf.fill()`]: #buffillvalue-offset-end-encoding
@@ -5653,6 +5742,7 @@ introducing security vulnerabilities into an application.
56535742
[`buffer.constants.MAX_STRING_LENGTH`]: #bufferconstantsmax_string_length
56545743
[`buffer.kMaxLength`]: #bufferkmaxlength
56555744
[`util.inspect()`]: util.md#utilinspectobject-options
5745+
[`v8.startupSnapshot.setDeserializeMainFunction()`]: v8.md#v8startupsnapshotsetdeserializemainfunctioncallback-data
56565746
[`v8::Uint8Array::kMaxLength`]: https://v8.github.io/api/head/classv8_1_1Uint8Array.html#a7677e3d0c9c92e4d40bef7212f5980c6
56575747
[base64url]: https://tools.ietf.org/html/rfc4648#section-5
56585748
[endianness]: https://en.wikipedia.org/wiki/Endianness

β€Ždoc/api/deprecations.mdβ€Ž

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4624,7 +4624,7 @@ will throw an error in a future version.
46244624
[`--pending-deprecation`]: cli.md#--pending-deprecation
46254625
[`--throw-deprecation`]: cli.md#--throw-deprecation
46264626
[`--unhandled-rejections`]: cli.md#--unhandled-rejectionsmode
4627-
[`Buffer.allocUnsafeSlow(size)`]: buffer.md#static-method-bufferallocunsafeslowsize
4627+
[`Buffer.allocUnsafeSlow(size)`]: buffer.md#static-method-bufferallocunsafeslowsize-alignment
46284628
[`Buffer.from(array)`]: buffer.md#static-method-bufferfromarray
46294629
[`Buffer.from(buffer)`]: buffer.md#static-method-bufferfrombuffer
46304630
[`Buffer.isBuffer()`]: buffer.md#static-method-bufferisbufferobj
@@ -4770,7 +4770,7 @@ will throw an error in a future version.
47704770
[`writable.writableLength`]: stream.md#writablewritablelength
47714771
[`zlib.bytesWritten`]: zlib.md#zlibbyteswritten
47724772
[alloc]: buffer.md#static-method-bufferallocsize-fill-encoding
4773-
[alloc_unsafe_size]: buffer.md#static-method-bufferallocunsafesize
4773+
[alloc_unsafe_size]: buffer.md#static-method-bufferallocunsafesize-alignment
47744774
[caveats of asynchronous customization hooks]: module.md#caveats-of-asynchronous-customization-hooks
47754775
[from_arraybuffer]: buffer.md#static-method-bufferfromarraybuffer-byteoffset-length
47764776
[from_string_encoding]: buffer.md#static-method-bufferfromstring-encoding

β€Ždoc/api/worker_threads.mdβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2234,7 +2234,7 @@ thread spawned will spawn another until the application crashes.
22342234
[`--max-old-space-size`]:cli.md#--max-old-space-sizesize-in-mib
22352235
[`--max-semi-space-size`]:cli.md#--max-semi-space-sizesize-in-mib
22362236
[`AsyncResource`]:async_hooks.md#class-asyncresource
2237-
[`Buffer.allocUnsafe()`]:buffer.md#static-method-bufferallocunsafesize
2237+
[`Buffer.allocUnsafe()`]:buffer.md#static-method-bufferallocunsafesize-alignment
22382238
[`ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST`]:errors.md#err_missing_message_port_in_transfer_list
22392239
[`ERR_WORKER_MESSAGING_ERRORED`]:errors.md#err_worker_messaging_errored
22402240
[`ERR_WORKER_MESSAGING_FAILED`]:errors.md#err_worker_messaging_failed

β€Žlib/buffer.jsβ€Ž

Lines changed: 77 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ const {
140140
markAsUntransferable,
141141
addBufferPrototypeMethods,
142142
createUnsafeBuffer,
143+
createUnsafeAlignedBuffer,
143144
asciiWrite,
144145
latin1Write,
145146
utf8Write,
@@ -171,13 +172,27 @@ const constants = ObjectDefineProperties({}, {
171172
},
172173
});
173174

175+
// The largest alignment accepted by `Buffer.allocUnsafeSlow()`. Any plausible
176+
// I/O alignment requirement (logical block size, memory page size, huge page
177+
// size) is well below this.
178+
constkMaxAlignment=2**30;
179+
180+
// Slices handed out of the pool are 8 byte aligned relative to the start of the
181+
// pool, so aligning the pool itself to a cache line keeps them from straddling
182+
// one more cache line than their size requires.
183+
constkPoolAlignment=64;
184+
174185
Buffer.poolSize=64*1024;
175-
letpoolSize,poolOffset,allocPool,allocBuffer;
186+
// `poolOffset` is relative to `poolBase`, which is where the pool starts inside
187+
// `allocPool`. The pool is over-allocated to be able to align it, so `poolBase`
188+
// is not necessarily 0.
189+
letpoolSize,poolOffset,poolBase,allocPool,allocBuffer;
176190

177191
functioncreatePool(){
178192
poolSize=Buffer.poolSize;
179-
allocBuffer=createUnsafeBuffer(poolSize);
193+
allocBuffer=createUnsafeAlignedBuffer(poolSize,kPoolAlignment);
180194
allocPool=allocBuffer.buffer;
195+
poolBase=TypedArrayPrototypeGetByteOffset(allocBuffer);
181196
markAsUntransferable(allocPool);
182197
poolOffset=0;
183198
}
@@ -444,40 +459,92 @@ Buffer.alloc = function alloc(size, fill, encoding) {
444459
/**
445460
* Equivalent to Buffer(num), by default creates a non-zero-filled Buffer
446461
* instance. If `--zero-fill-buffers` is set, will zero-fill the buffer.
462+
*
463+
* If `alignment` is given, the memory backing the returned buffer starts at an
464+
* address that is a multiple of `alignment`. See `Buffer.allocUnsafeSlow()`.
465+
* @param {number} size
466+
* @param {number} [alignment] A power of two, at most 2 ** 30
447467
* @returns {FastBuffer}
448468
*/
449-
Buffer.allocUnsafe=functionallocUnsafe(size){
469+
Buffer.allocUnsafe=functionallocUnsafe(size,alignment){
450470
validateNumber(size,'size',0,kMaxLength);
451-
returnallocate(size);
471+
if(alignment===undefined){
472+
returnallocate(size);
473+
}
474+
validateAlignment(size,alignment);
475+
returnallocateAligned(size,alignment);
452476
};
453477

454478
/**
455479
* By default creates a non-zero-filled Buffer instance that is not allocated
456480
* off the pre-initialized pool. If `--zero-fill-buffers` is set, will zero-fill
457481
* the buffer.
482+
*
483+
* If `alignment` is given, the memory backing the returned buffer starts at an
484+
* address that is a multiple of `alignment`, which is required by e.g. reads
485+
* and writes on file descriptors opened with `O_DIRECT`. Note that up to
486+
* `alignment - 1` extra bytes are allocated to satisfy the request, and that
487+
* the returned buffer's `byteOffset` is therefore usually non-zero.
458488
* @param {number} size
459-
* @returns {FastBuffer|undefined}
489+
* @param {number} [alignment] A power of two, at most 2 ** 30
490+
* @returns {FastBuffer}
460491
*/
461-
Buffer.allocUnsafeSlow=functionallocUnsafeSlow(size){
492+
Buffer.allocUnsafeSlow=functionallocUnsafeSlow(size,alignment){
462493
validateNumber(size,'size',0,kMaxLength);
463-
returncreateUnsafeBuffer(size);
494+
if(alignment===undefined){
495+
returncreateUnsafeBuffer(size);
496+
}
497+
validateAlignment(size,alignment);
498+
returncreateUnsafeAlignedBuffer(size,alignment);
464499
};
465500

501+
functionvalidateAlignment(size,alignment){
502+
validateInteger(alignment,'alignment',1,kMaxAlignment);
503+
if((alignment&(alignment-1))!==0){
504+
thrownewERR_INVALID_ARG_VALUE(
505+
'alignment',alignment,'must be a power of two');
506+
}
507+
// Satisfying the alignment costs up to `alignment - 1` extra bytes.
508+
if(size>kMaxLength-(alignment-1)){
509+
thrownewERR_OUT_OF_RANGE(
510+
'size',`<= ${kMaxLength-(alignment-1)}`,size);
511+
}
512+
}
513+
466514
functionallocate(size){
467515
if(size<=0){
468516
returnnewFastBuffer();
469517
}
470518
if(size<(Buffer.poolSize>>>1)){
471519
if(size>(poolSize-poolOffset))
472520
createPool();
473-
constb=newFastBuffer(allocPool,poolOffset,size);
521+
constb=newFastBuffer(allocPool,poolBase+poolOffset,size);
474522
poolOffset+=size;
475523
alignPool();
476524
returnb;
477525
}
478526
returncreateUnsafeBuffer(size);
479527
}
480528

529+
functionallocateAligned(size,alignment){
530+
if(size<=0){
531+
returnnewFastBuffer();
532+
}
533+
// The pool starts at a `kPoolAlignment` aligned address, so any alignment up
534+
// to that can be satisfied by padding the offset into the pool. Stricter
535+
// alignments need an allocation of their own.
536+
if(alignment>kPoolAlignment||size>=(Buffer.poolSize>>>1)){
537+
returncreateUnsafeAlignedBuffer(size,alignment);
538+
}
539+
poolOffset=(poolOffset+alignment-1)&~(alignment-1);
540+
if(size>(poolSize-poolOffset))
541+
createPool();
542+
constb=newFastBuffer(allocPool,poolBase+poolOffset,size);
543+
poolOffset+=size;
544+
alignPool();
545+
returnb;
546+
}
547+
481548
functionfromStringFast(string,ops){
482549
constmaxLength=Buffer.poolSize>>>1;
483550

@@ -498,7 +565,7 @@ function fromStringFast(string, ops) {
498565
createPool();
499566

500567
constactual=ops.write(allocBuffer,string,poolOffset,length);
501-
constb=newFastBuffer(allocPool,poolOffset,actual);
568+
constb=newFastBuffer(allocPool,poolBase+poolOffset,actual);
502569

503570
poolOffset+=actual;
504571
alignPool();
@@ -560,7 +627,7 @@ function fromArrayLike(obj) {
560627
if(length<(Buffer.poolSize>>>1)){
561628
if(length>(poolSize-poolOffset))
562629
createPool();
563-
constb=newFastBuffer(allocPool,poolOffset,length);
630+
constb=newFastBuffer(allocPool,poolBase+poolOffset,length);
564631
TypedArrayPrototypeSet(b,obj,0);
565632
poolOffset+=length;
566633
alignPool();

β€Žlib/internal/buffer.jsβ€Ž

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const {
3333
hexWrite,
3434
ucs2Write,
3535
utf8WriteStatic,
36+
arrayBufferAlignedOffset,
3637
createUnsafeArrayBuffer,
3738
setDetachKey,
3839
}=internalBinding('buffer');
@@ -1104,12 +1105,28 @@ function createUnsafeBuffer(size) {
11041105
returnnewFastBuffer(createUnsafeArrayBuffer(size));
11051106
}
11061107

1108+
// Returns an uninitialized buffer of `size` bytes whose first byte is located at
1109+
// a memory address that is a multiple of `alignment`. `alignment` must be a
1110+
// power of two, and `size + alignment - 1` must not exceed the maximum buffer
1111+
// length. Since the address of a backing store cannot be chosen, `alignment - 1`
1112+
// extra bytes are allocated and skipped, which leaves the returned buffer with a
1113+
// non-zero `byteOffset` into a larger ArrayBuffer.
1114+
functioncreateUnsafeAlignedBuffer(size,alignment){
1115+
if(size===0){
1116+
returnnewFastBuffer();
1117+
}
1118+
1119+
constab=createUnsafeArrayBuffer(size+alignment-1);
1120+
returnnewFastBuffer(ab,arrayBufferAlignedOffset(ab,alignment),size);
1121+
}
1122+
11071123
module.exports={
11081124
FastBuffer,
11091125
addBufferPrototypeMethods,
11101126
markAsUntransferable,
11111127
isMarkedAsUntransferable,
11121128
createUnsafeBuffer,
1129+
createUnsafeAlignedBuffer,
11131130
readUInt16BE,
11141131
readUInt32BE,
11151132
asciiWrite,

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit 615273d

Browse files
ronagclaude
authored andcommitted
buffer: support aligned allocations
Add an optional `alignment` argument to `Buffer.allocUnsafe()` and `Buffer.allocUnsafeSlow()`, which guarantees that the memory backing the returned buffer starts at an address that is a multiple of `alignment`. Some operating system interfaces refuse to work with unaligned memory. The motivating case is unbuffered ("direct") file I/O: a read or write on a descriptor opened with `O_DIRECT` fails with `EINVAL` unless the buffer address is a multiple of the logical block size of the underlying device. Until now there was no way to obtain such a buffer from JS, since the address of a backing store can neither be observed nor chosen. Alignment is also worth having purely for performance, for instance to keep a hot buffer from straddling one more cache line than its size requires. V8 does not allow picking the address of a backing store, so alignment is instead achieved by over-allocating `alignment - 1` bytes and positioning the buffer at the first suitably aligned byte within them. The new `arrayBufferAlignedOffset()` binding computes that offset. Addresses are not stable across snapshot serialization, so the binding reports no padding while a snapshot is being built. Otherwise the snapshot would capture where this particular process happened to allocate and stop being reproducible. Buffers restored from a snapshot are consequently not aligned; the pool works around this by recreating itself in a deserialize callback. The `Buffer.allocUnsafe()` pool is now aligned to a cache line itself, which lets pooled allocations satisfy any alignment up to 64 bytes by padding their offset into the pool rather than allocating separately. Assisted-by: Claude/Opus 5 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Robert Nagy <ronagy@icloud.com> PR-URL: #65003 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
1 parent 49fb028 commit 615273d

7 files changed

Lines changed: 415 additions & 35 deletions

File tree

β€Ždoc/api/buffer.mdβ€Ž

Lines changed: 94 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -791,11 +791,14 @@ data that might not have been allocated for `Buffer`s.
791791

792792
A `TypeError` will be thrown if `size` is not a number.
793793

794-
### Static method: `Buffer.allocUnsafe(size)`
794+
### Static method: `Buffer.allocUnsafe(size[, alignment])`
795795

796796
<!-- YAML
797797
added: v5.10.0
798798
changes:
799+
- version: REPLACEME
800+
pr-url: https://github.com/nodejs/node/pull/65003
801+
description: Added the `alignment` argument.
799802
- version: v20.0.0
800803
pr-url: https://github.com/nodejs/node/pull/45796
801804
description: Throw ERR_INVALID_ARG_TYPE or ERR_OUT_OF_RANGE instead of
@@ -810,6 +813,9 @@ changes:
810813
-->
811814

812815
*`size` {integer} The desired length of the new `Buffer`.
816+
*`alignment` {integer} If given, the memory backing the new `Buffer` will start
817+
at an address that is a multiple of `alignment`. Must be a power of two no
818+
larger than `2 ** 30`. See [Aligned allocations][].
813819
* Returns: {Buffer}
814820

815821
Allocates a new `Buffer` of `size` bytes. If `size` is larger than
@@ -865,11 +871,14 @@ pool, while `Buffer.allocUnsafe(size).fill(fill)` _will_ use the internal
865871
difference is subtle but can be important when an application requires the
866872
additional performance that [`Buffer.allocUnsafe()`][] provides.
867873

868-
### Static method: `Buffer.allocUnsafeSlow(size)`
874+
### Static method: `Buffer.allocUnsafeSlow(size[, alignment])`
869875

870876
<!-- YAML
871877
added: v5.12.0
872878
changes:
879+
- version: REPLACEME
880+
pr-url: https://github.com/nodejs/node/pull/65003
881+
description: Added the `alignment` argument.
873882
- version: v20.0.0
874883
pr-url: https://github.com/nodejs/node/pull/45796
875884
description: Throw ERR_INVALID_ARG_TYPE or ERR_OUT_OF_RANGE instead of
@@ -881,6 +890,9 @@ changes:
881890
-->
882891

883892
*`size` {integer} The desired length of the new `Buffer`.
893+
*`alignment` {integer} If given, the memory backing the new `Buffer` will start
894+
at an address that is a multiple of `alignment`. Must be a power of two no
895+
larger than `2 ** 30`. See [Aligned allocations][].
884896
* Returns: {Buffer}
885897

886898
Allocates a new `Buffer` of `size` bytes. If `size` is larger than
@@ -5608,16 +5620,92 @@ While there are clear performance advantages to using
56085620
[`Buffer.allocUnsafe()`][], extra care _must_ be taken in order to avoid
56095621
introducing security vulnerabilities into an application.
56105622

5623+
### Aligned allocations
5624+
5625+
Some operating system interfaces require the memory they operate on to be
5626+
aligned, and on some hardware alignment is merely faster. The most common
5627+
example of the former is unbuffered ("direct") file I/O, which on Linux requires
5628+
the buffer address, the file offset and the transfer length to all be multiples
5629+
of the logical block size of the underlying device:
5630+
5631+
```mjs
5632+
import { open } from'node:fs/promises';
5633+
import { constants } from'node:fs';
5634+
import { Buffer } from'node:buffer';
5635+
5636+
constblockSize=4096;
5637+
5638+
// The buffer address must be block-aligned for O_DIRECT to accept it.
5639+
constbuf=Buffer.allocUnsafeSlow(blockSize, blockSize);
5640+
5641+
constfile=awaitopen('/dev/sda', constants.O_RDONLY|constants.O_DIRECT);
5642+
try {
5643+
awaitfile.read(buf, 0, blockSize, 0);
5644+
} finally {
5645+
awaitfile.close();
5646+
}
5647+
```
5648+
5649+
```cjs
5650+
constfs=require('node:fs');
5651+
const { Buffer } =require('node:buffer');
5652+
5653+
constblockSize=4096;
5654+
5655+
// The buffer address must be block-aligned for O_DIRECT to accept it.
5656+
constbuf=Buffer.allocUnsafeSlow(blockSize, blockSize);
5657+
5658+
constflags=fs.constants.O_RDONLY|fs.constants.O_DIRECT;
5659+
fs.open('/dev/sda', flags, (err, fd) => {
5660+
if (err) throw err;
5661+
fs.read(fd, buf, 0, blockSize, 0, (err) => {
5662+
fs.close(fd, () => {});
5663+
if (err) throw err;
5664+
});
5665+
});
5666+
```
5667+
5668+
Alignment can also be worth requesting purely for performance, even when no
5669+
interface demands it. Aligning a hot `Buffer` to the cache line size (64 bytes on
5670+
most contemporary CPUs) keeps it from straddling one more cache line than it
5671+
needs to, so that a small structure is fetched with one cache miss instead of
5672+
two, and page-aligned (4096 bytes) allocations similarly help interfaces that map
5673+
or pin memory. These are micro-optimizations: measure before reaching for them,
5674+
since the extra bytes are not free.
5675+
5676+
Because the address of a `Buffer`'s memory cannot be chosen directly, extra bytes
5677+
have to be allocated or skipped to reach an aligned address.
5678+
[`Buffer.allocUnsafeSlow()`][] over-allocates up to `alignment - 1` bytes and
5679+
positions the returned `Buffer` at the first suitably aligned byte within them.
5680+
[`Buffer.allocUnsafe()`][] instead pads its offset into the shared internal pool,
5681+
whose start is always aligned to 64 bytes, and only falls back to an allocation
5682+
of its own when `alignment` is larger than that. Either way,
5683+
[`buf.byteOffset`][] is usually not 0 and [`buf.buffer`][] is larger than `size`,
5684+
so code that reaches past the `Buffer` into its underlying `ArrayBuffer` must
5685+
take the offset into account, as it must for pooled `Buffer`s.
5686+
5687+
The alignment is a property of the returned `Buffer` and is preserved for its
5688+
whole lifetime, but it is not inherited by other views: [`buf.subarray`][],
5689+
[`buf.slice()`][] and `structuredClone()` may all produce unaligned `Buffer`s.
5690+
5691+
Alignment also does not survive being captured in a startup snapshot: memory does
5692+
not keep its address across serialization, so a `Buffer` allocated while
5693+
[`--build-snapshot`][] is in effect is not aligned in the deserialized process.
5694+
Allocate inside a [`v8.startupSnapshot.setDeserializeMainFunction()`][] callback,
5695+
or after startup, if the alignment has to hold at run time.
5696+
56115697
[ASCII]: https://en.wikipedia.org/wiki/ASCII
5698+
[Aligned allocations]: #aligned-allocations
56125699
[Base64]: https://en.wikipedia.org/wiki/Base64
56135700
[ISO-8859-1]: https://en.wikipedia.org/wiki/ISO-8859-1
56145701
[RFC 4648, Section 5]: https://tools.ietf.org/html/rfc4648#section-5
56155702
[UTF-16]: https://en.wikipedia.org/wiki/UTF-16
56165703
[UTF-8]: https://en.wikipedia.org/wiki/UTF-8
56175704
[WHATWG Encoding Standard]: https://encoding.spec.whatwg.org/
5705+
[`--build-snapshot`]: cli.md#--build-snapshot
56185706
[`Buffer.alloc()`]: #static-method-bufferallocsize-fill-encoding
5619-
[`Buffer.allocUnsafe()`]: #static-method-bufferallocunsafesize
5620-
[`Buffer.allocUnsafeSlow()`]: #static-method-bufferallocunsafeslowsize
5707+
[`Buffer.allocUnsafe()`]: #static-method-bufferallocunsafesize-alignment
5708+
[`Buffer.allocUnsafeSlow()`]: #static-method-bufferallocunsafeslowsize-alignment
56215709
[`Buffer.concat()`]: #static-method-bufferconcatlist-totallength
56225710
[`Buffer.copyBytesFrom()`]: #static-method-buffercopybytesfromview-offset-length
56235711
[`Buffer.from(array)`]: #static-method-bufferfromarray
@@ -5639,6 +5727,7 @@ introducing security vulnerabilities into an application.
56395727
[`TypedArray.prototype.subarray()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/subarray
56405728
[`blob.stream()`]: #blobstream
56415729
[`buf.buffer`]: #bufbuffer
5730+
[`buf.byteOffset`]: #bufbyteoffset
56425731
[`buf.compare()`]: #bufcomparetarget-targetstart-targetend-sourcestart-sourceend
56435732
[`buf.entries()`]: #bufentries
56445733
[`buf.fill()`]: #buffillvalue-offset-end-encoding
@@ -5653,6 +5742,7 @@ introducing security vulnerabilities into an application.
56535742
[`buffer.constants.MAX_STRING_LENGTH`]: #bufferconstantsmax_string_length
56545743
[`buffer.kMaxLength`]: #bufferkmaxlength
56555744
[`util.inspect()`]: util.md#utilinspectobject-options
5745+
[`v8.startupSnapshot.setDeserializeMainFunction()`]: v8.md#v8startupsnapshotsetdeserializemainfunctioncallback-data
56565746
[`v8::Uint8Array::kMaxLength`]: https://v8.github.io/api/head/classv8_1_1Uint8Array.html#a7677e3d0c9c92e4d40bef7212f5980c6
56575747
[base64url]: https://tools.ietf.org/html/rfc4648#section-5
56585748
[endianness]: https://en.wikipedia.org/wiki/Endianness

β€Ždoc/api/deprecations.mdβ€Ž

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4624,7 +4624,7 @@ will throw an error in a future version.
46244624
[`--pending-deprecation`]: cli.md#--pending-deprecation
46254625
[`--throw-deprecation`]: cli.md#--throw-deprecation
46264626
[`--unhandled-rejections`]: cli.md#--unhandled-rejectionsmode
4627-
[`Buffer.allocUnsafeSlow(size)`]: buffer.md#static-method-bufferallocunsafeslowsize
4627+
[`Buffer.allocUnsafeSlow(size)`]: buffer.md#static-method-bufferallocunsafeslowsize-alignment
46284628
[`Buffer.from(array)`]: buffer.md#static-method-bufferfromarray
46294629
[`Buffer.from(buffer)`]: buffer.md#static-method-bufferfrombuffer
46304630
[`Buffer.isBuffer()`]: buffer.md#static-method-bufferisbufferobj
@@ -4770,7 +4770,7 @@ will throw an error in a future version.
47704770
[`writable.writableLength`]: stream.md#writablewritablelength
47714771
[`zlib.bytesWritten`]: zlib.md#zlibbyteswritten
47724772
[alloc]: buffer.md#static-method-bufferallocsize-fill-encoding
4773-
[alloc_unsafe_size]: buffer.md#static-method-bufferallocunsafesize
4773+
[alloc_unsafe_size]: buffer.md#static-method-bufferallocunsafesize-alignment
47744774
[caveats of asynchronous customization hooks]: module.md#caveats-of-asynchronous-customization-hooks
47754775
[from_arraybuffer]: buffer.md#static-method-bufferfromarraybuffer-byteoffset-length
47764776
[from_string_encoding]: buffer.md#static-method-bufferfromstring-encoding

β€Ždoc/api/worker_threads.mdβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2234,7 +2234,7 @@ thread spawned will spawn another until the application crashes.
22342234
[`--max-old-space-size`]:cli.md#--max-old-space-sizesize-in-mib
22352235
[`--max-semi-space-size`]:cli.md#--max-semi-space-sizesize-in-mib
22362236
[`AsyncResource`]:async_hooks.md#class-asyncresource
2237-
[`Buffer.allocUnsafe()`]:buffer.md#static-method-bufferallocunsafesize
2237+
[`Buffer.allocUnsafe()`]:buffer.md#static-method-bufferallocunsafesize-alignment
22382238
[`ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST`]:errors.md#err_missing_message_port_in_transfer_list
22392239
[`ERR_WORKER_MESSAGING_ERRORED`]:errors.md#err_worker_messaging_errored
22402240
[`ERR_WORKER_MESSAGING_FAILED`]:errors.md#err_worker_messaging_failed

β€Žlib/buffer.jsβ€Ž

Lines changed: 77 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ const {
140140
markAsUntransferable,
141141
addBufferPrototypeMethods,
142142
createUnsafeBuffer,
143+
createUnsafeAlignedBuffer,
143144
asciiWrite,
144145
latin1Write,
145146
utf8Write,
@@ -171,13 +172,27 @@ const constants = ObjectDefineProperties({}, {
171172
},
172173
});
173174

175+
// The largest alignment accepted by `Buffer.allocUnsafeSlow()`. Any plausible
176+
// I/O alignment requirement (logical block size, memory page size, huge page
177+
// size) is well below this.
178+
constkMaxAlignment=2**30;
179+
180+
// Slices handed out of the pool are 8 byte aligned relative to the start of the
181+
// pool, so aligning the pool itself to a cache line keeps them from straddling
182+
// one more cache line than their size requires.
183+
constkPoolAlignment=64;
184+
174185
Buffer.poolSize=64*1024;
175-
letpoolSize,poolOffset,allocPool,allocBuffer;
186+
// `poolOffset` is relative to `poolBase`, which is where the pool starts inside
187+
// `allocPool`. The pool is over-allocated to be able to align it, so `poolBase`
188+
// is not necessarily 0.
189+
letpoolSize,poolOffset,poolBase,allocPool,allocBuffer;
176190

177191
functioncreatePool(){
178192
poolSize=Buffer.poolSize;
179-
allocBuffer=createUnsafeBuffer(poolSize);
193+
allocBuffer=createUnsafeAlignedBuffer(poolSize,kPoolAlignment);
180194
allocPool=allocBuffer.buffer;
195+
poolBase=TypedArrayPrototypeGetByteOffset(allocBuffer);
181196
markAsUntransferable(allocPool);
182197
poolOffset=0;
183198
}
@@ -444,40 +459,92 @@ Buffer.alloc = function alloc(size, fill, encoding) {
444459
/**
445460
* Equivalent to Buffer(num), by default creates a non-zero-filled Buffer
446461
* instance. If `--zero-fill-buffers` is set, will zero-fill the buffer.
462+
*
463+
* If `alignment` is given, the memory backing the returned buffer starts at an
464+
* address that is a multiple of `alignment`. See `Buffer.allocUnsafeSlow()`.
465+
* @param {number} size
466+
* @param {number} [alignment] A power of two, at most 2 ** 30
447467
* @returns {FastBuffer}
448468
*/
449-
Buffer.allocUnsafe=functionallocUnsafe(size){
469+
Buffer.allocUnsafe=functionallocUnsafe(size,alignment){
450470
validateNumber(size,'size',0,kMaxLength);
451-
returnallocate(size);
471+
if(alignment===undefined){
472+
returnallocate(size);
473+
}
474+
validateAlignment(size,alignment);
475+
returnallocateAligned(size,alignment);
452476
};
453477

454478
/**
455479
* By default creates a non-zero-filled Buffer instance that is not allocated
456480
* off the pre-initialized pool. If `--zero-fill-buffers` is set, will zero-fill
457481
* the buffer.
482+
*
483+
* If `alignment` is given, the memory backing the returned buffer starts at an
484+
* address that is a multiple of `alignment`, which is required by e.g. reads
485+
* and writes on file descriptors opened with `O_DIRECT`. Note that up to
486+
* `alignment - 1` extra bytes are allocated to satisfy the request, and that
487+
* the returned buffer's `byteOffset` is therefore usually non-zero.
458488
* @param {number} size
459-
* @returns {FastBuffer|undefined}
489+
* @param {number} [alignment] A power of two, at most 2 ** 30
490+
* @returns {FastBuffer}
460491
*/
461-
Buffer.allocUnsafeSlow=functionallocUnsafeSlow(size){
492+
Buffer.allocUnsafeSlow=functionallocUnsafeSlow(size,alignment){
462493
validateNumber(size,'size',0,kMaxLength);
463-
returncreateUnsafeBuffer(size);
494+
if(alignment===undefined){
495+
returncreateUnsafeBuffer(size);
496+
}
497+
validateAlignment(size,alignment);
498+
returncreateUnsafeAlignedBuffer(size,alignment);
464499
};
465500

501+
functionvalidateAlignment(size,alignment){
502+
validateInteger(alignment,'alignment',1,kMaxAlignment);
503+
if((alignment&(alignment-1))!==0){
504+
thrownewERR_INVALID_ARG_VALUE(
505+
'alignment',alignment,'must be a power of two');
506+
}
507+
// Satisfying the alignment costs up to `alignment - 1` extra bytes.
508+
if(size>kMaxLength-(alignment-1)){
509+
thrownewERR_OUT_OF_RANGE(
510+
'size',`<= ${kMaxLength-(alignment-1)}`,size);
511+
}
512+
}
513+
466514
functionallocate(size){
467515
if(size<=0){
468516
returnnewFastBuffer();
469517
}
470518
if(size<(Buffer.poolSize>>>1)){
471519
if(size>(poolSize-poolOffset))
472520
createPool();
473-
constb=newFastBuffer(allocPool,poolOffset,size);
521+
constb=newFastBuffer(allocPool,poolBase+poolOffset,size);
474522
poolOffset+=size;
475523
alignPool();
476524
returnb;
477525
}
478526
returncreateUnsafeBuffer(size);
479527
}
480528

529+
functionallocateAligned(size,alignment){
530+
if(size<=0){
531+
returnnewFastBuffer();
532+
}
533+
// The pool starts at a `kPoolAlignment` aligned address, so any alignment up
534+
// to that can be satisfied by padding the offset into the pool. Stricter
535+
// alignments need an allocation of their own.
536+
if(alignment>kPoolAlignment||size>=(Buffer.poolSize>>>1)){
537+
returncreateUnsafeAlignedBuffer(size,alignment);
538+
}
539+
poolOffset=(poolOffset+alignment-1)&~(alignment-1);
540+
if(size>(poolSize-poolOffset))
541+
createPool();
542+
constb=newFastBuffer(allocPool,poolBase+poolOffset,size);
543+
poolOffset+=size;
544+
alignPool();
545+
returnb;
546+
}
547+
481548
functionfromStringFast(string,ops){
482549
constmaxLength=Buffer.poolSize>>>1;
483550

@@ -498,7 +565,7 @@ function fromStringFast(string, ops) {
498565
createPool();
499566

500567
constactual=ops.write(allocBuffer,string,poolOffset,length);
501-
constb=newFastBuffer(allocPool,poolOffset,actual);
568+
constb=newFastBuffer(allocPool,poolBase+poolOffset,actual);
502569

503570
poolOffset+=actual;
504571
alignPool();
@@ -560,7 +627,7 @@ function fromArrayLike(obj) {
560627
if(length<(Buffer.poolSize>>>1)){
561628
if(length>(poolSize-poolOffset))
562629
createPool();
563-
constb=newFastBuffer(allocPool,poolOffset,length);
630+
constb=newFastBuffer(allocPool,poolBase+poolOffset,length);
564631
TypedArrayPrototypeSet(b,obj,0);
565632
poolOffset+=length;
566633
alignPool();

β€Žlib/internal/buffer.jsβ€Ž

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const {
3333
hexWrite,
3434
ucs2Write,
3535
utf8WriteStatic,
36+
arrayBufferAlignedOffset,
3637
createUnsafeArrayBuffer,
3738
setDetachKey,
3839
}=internalBinding('buffer');
@@ -1104,12 +1105,28 @@ function createUnsafeBuffer(size) {
11041105
returnnewFastBuffer(createUnsafeArrayBuffer(size));
11051106
}
11061107

1108+
// Returns an uninitialized buffer of `size` bytes whose first byte is located at
1109+
// a memory address that is a multiple of `alignment`. `alignment` must be a
1110+
// power of two, and `size + alignment - 1` must not exceed the maximum buffer
1111+
// length. Since the address of a backing store cannot be chosen, `alignment - 1`
1112+
// extra bytes are allocated and skipped, which leaves the returned buffer with a
1113+
// non-zero `byteOffset` into a larger ArrayBuffer.
1114+
functioncreateUnsafeAlignedBuffer(size,alignment){
1115+
if(size===0){
1116+
returnnewFastBuffer();
1117+
}
1118+
1119+
constab=createUnsafeArrayBuffer(size+alignment-1);
1120+
returnnewFastBuffer(ab,arrayBufferAlignedOffset(ab,alignment),size);
1121+
}
1122+
11071123
module.exports={
11081124
FastBuffer,
11091125
addBufferPrototypeMethods,
11101126
markAsUntransferable,
11111127
isMarkedAsUntransferable,
11121128
createUnsafeBuffer,
1129+
createUnsafeAlignedBuffer,
11131130
readUInt16BE,
11141131
readUInt32BE,
11151132
asciiWrite,

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Commit 615273d

Browse files
ronagclaude
authored andcommitted
buffer: support aligned allocations
Add an optional `alignment` argument to `Buffer.allocUnsafe()` and `Buffer.allocUnsafeSlow()`, which guarantees that the memory backing the returned buffer starts at an address that is a multiple of `alignment`. Some operating system interfaces refuse to work with unaligned memory. The motivating case is unbuffered ("direct") file I/O: a read or write on a descriptor opened with `O_DIRECT` fails with `EINVAL` unless the buffer address is a multiple of the logical block size of the underlying device. Until now there was no way to obtain such a buffer from JS, since the address of a backing store can neither be observed nor chosen. Alignment is also worth having purely for performance, for instance to keep a hot buffer from straddling one more cache line than its size requires. V8 does not allow picking the address of a backing store, so alignment is instead achieved by over-allocating `alignment - 1` bytes and positioning the buffer at the first suitably aligned byte within them. The new `arrayBufferAlignedOffset()` binding computes that offset. Addresses are not stable across snapshot serialization, so the binding reports no padding while a snapshot is being built. Otherwise the snapshot would capture where this particular process happened to allocate and stop being reproducible. Buffers restored from a snapshot are consequently not aligned; the pool works around this by recreating itself in a deserialize callback. The `Buffer.allocUnsafe()` pool is now aligned to a cache line itself, which lets pooled allocations satisfy any alignment up to 64 bytes by padding their offset into the pool rather than allocating separately. Assisted-by: Claude/Opus 5 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Robert Nagy <ronagy@icloud.com> PR-URL: #65003 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
1 parent 49fb028 commit 615273d

7 files changed

Lines changed: 415 additions & 35 deletions

File tree

β€Ždoc/api/buffer.mdβ€Ž

Lines changed: 94 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -791,11 +791,14 @@ data that might not have been allocated for `Buffer`s.
791791

792792
A `TypeError` will be thrown if `size` is not a number.
793793

794-
### Static method: `Buffer.allocUnsafe(size)`
794+
### Static method: `Buffer.allocUnsafe(size[, alignment])`
795795

796796
<!-- YAML
797797
added: v5.10.0
798798
changes:
799+
- version: REPLACEME
800+
pr-url: https://github.com/nodejs/node/pull/65003
801+
description: Added the `alignment` argument.
799802
- version: v20.0.0
800803
pr-url: https://github.com/nodejs/node/pull/45796
801804
description: Throw ERR_INVALID_ARG_TYPE or ERR_OUT_OF_RANGE instead of
@@ -810,6 +813,9 @@ changes:
810813
-->
811814

812815
*`size` {integer} The desired length of the new `Buffer`.
816+
*`alignment` {integer} If given, the memory backing the new `Buffer` will start
817+
at an address that is a multiple of `alignment`. Must be a power of two no
818+
larger than `2 ** 30`. See [Aligned allocations][].
813819
* Returns: {Buffer}
814820

815821
Allocates a new `Buffer` of `size` bytes. If `size` is larger than
@@ -865,11 +871,14 @@ pool, while `Buffer.allocUnsafe(size).fill(fill)` _will_ use the internal
865871
difference is subtle but can be important when an application requires the
866872
additional performance that [`Buffer.allocUnsafe()`][] provides.
867873

868-
### Static method: `Buffer.allocUnsafeSlow(size)`
874+
### Static method: `Buffer.allocUnsafeSlow(size[, alignment])`
869875

870876
<!-- YAML
871877
added: v5.12.0
872878
changes:
879+
- version: REPLACEME
880+
pr-url: https://github.com/nodejs/node/pull/65003
881+
description: Added the `alignment` argument.
873882
- version: v20.0.0
874883
pr-url: https://github.com/nodejs/node/pull/45796
875884
description: Throw ERR_INVALID_ARG_TYPE or ERR_OUT_OF_RANGE instead of
@@ -881,6 +890,9 @@ changes:
881890
-->
882891

883892
*`size` {integer} The desired length of the new `Buffer`.
893+
*`alignment` {integer} If given, the memory backing the new `Buffer` will start
894+
at an address that is a multiple of `alignment`. Must be a power of two no
895+
larger than `2 ** 30`. See [Aligned allocations][].
884896
* Returns: {Buffer}
885897

886898
Allocates a new `Buffer` of `size` bytes. If `size` is larger than
@@ -5608,16 +5620,92 @@ While there are clear performance advantages to using
56085620
[`Buffer.allocUnsafe()`][], extra care _must_ be taken in order to avoid
56095621
introducing security vulnerabilities into an application.
56105622

5623+
### Aligned allocations
5624+
5625+
Some operating system interfaces require the memory they operate on to be
5626+
aligned, and on some hardware alignment is merely faster. The most common
5627+
example of the former is unbuffered ("direct") file I/O, which on Linux requires
5628+
the buffer address, the file offset and the transfer length to all be multiples
5629+
of the logical block size of the underlying device:
5630+
5631+
```mjs
5632+
import { open } from'node:fs/promises';
5633+
import { constants } from'node:fs';
5634+
import { Buffer } from'node:buffer';
5635+
5636+
constblockSize=4096;
5637+
5638+
// The buffer address must be block-aligned for O_DIRECT to accept it.
5639+
constbuf=Buffer.allocUnsafeSlow(blockSize, blockSize);
5640+
5641+
constfile=awaitopen('/dev/sda', constants.O_RDONLY|constants.O_DIRECT);
5642+
try {
5643+
awaitfile.read(buf, 0, blockSize, 0);
5644+
} finally {
5645+
awaitfile.close();
5646+
}
5647+
```
5648+
5649+
```cjs
5650+
constfs=require('node:fs');
5651+
const { Buffer } =require('node:buffer');
5652+
5653+
constblockSize=4096;
5654+
5655+
// The buffer address must be block-aligned for O_DIRECT to accept it.
5656+
constbuf=Buffer.allocUnsafeSlow(blockSize, blockSize);
5657+
5658+
constflags=fs.constants.O_RDONLY|fs.constants.O_DIRECT;
5659+
fs.open('/dev/sda', flags, (err, fd) => {
5660+
if (err) throw err;
5661+
fs.read(fd, buf, 0, blockSize, 0, (err) => {
5662+
fs.close(fd, () => {});
5663+
if (err) throw err;
5664+
});
5665+
});
5666+
```
5667+
5668+
Alignment can also be worth requesting purely for performance, even when no
5669+
interface demands it. Aligning a hot `Buffer` to the cache line size (64 bytes on
5670+
most contemporary CPUs) keeps it from straddling one more cache line than it
5671+
needs to, so that a small structure is fetched with one cache miss instead of
5672+
two, and page-aligned (4096 bytes) allocations similarly help interfaces that map
5673+
or pin memory. These are micro-optimizations: measure before reaching for them,
5674+
since the extra bytes are not free.
5675+
5676+
Because the address of a `Buffer`'s memory cannot be chosen directly, extra bytes
5677+
have to be allocated or skipped to reach an aligned address.
5678+
[`Buffer.allocUnsafeSlow()`][] over-allocates up to `alignment - 1` bytes and
5679+
positions the returned `Buffer` at the first suitably aligned byte within them.
5680+
[`Buffer.allocUnsafe()`][] instead pads its offset into the shared internal pool,
5681+
whose start is always aligned to 64 bytes, and only falls back to an allocation
5682+
of its own when `alignment` is larger than that. Either way,
5683+
[`buf.byteOffset`][] is usually not 0 and [`buf.buffer`][] is larger than `size`,
5684+
so code that reaches past the `Buffer` into its underlying `ArrayBuffer` must
5685+
take the offset into account, as it must for pooled `Buffer`s.
5686+
5687+
The alignment is a property of the returned `Buffer` and is preserved for its
5688+
whole lifetime, but it is not inherited by other views: [`buf.subarray`][],
5689+
[`buf.slice()`][] and `structuredClone()` may all produce unaligned `Buffer`s.
5690+
5691+
Alignment also does not survive being captured in a startup snapshot: memory does
5692+
not keep its address across serialization, so a `Buffer` allocated while
5693+
[`--build-snapshot`][] is in effect is not aligned in the deserialized process.
5694+
Allocate inside a [`v8.startupSnapshot.setDeserializeMainFunction()`][] callback,
5695+
or after startup, if the alignment has to hold at run time.
5696+
56115697
[ASCII]: https://en.wikipedia.org/wiki/ASCII
5698+
[Aligned allocations]: #aligned-allocations
56125699
[Base64]: https://en.wikipedia.org/wiki/Base64
56135700
[ISO-8859-1]: https://en.wikipedia.org/wiki/ISO-8859-1
56145701
[RFC 4648, Section 5]: https://tools.ietf.org/html/rfc4648#section-5
56155702
[UTF-16]: https://en.wikipedia.org/wiki/UTF-16
56165703
[UTF-8]: https://en.wikipedia.org/wiki/UTF-8
56175704
[WHATWG Encoding Standard]: https://encoding.spec.whatwg.org/
5705+
[`--build-snapshot`]: cli.md#--build-snapshot
56185706
[`Buffer.alloc()`]: #static-method-bufferallocsize-fill-encoding
5619-
[`Buffer.allocUnsafe()`]: #static-method-bufferallocunsafesize
5620-
[`Buffer.allocUnsafeSlow()`]: #static-method-bufferallocunsafeslowsize
5707+
[`Buffer.allocUnsafe()`]: #static-method-bufferallocunsafesize-alignment
5708+
[`Buffer.allocUnsafeSlow()`]: #static-method-bufferallocunsafeslowsize-alignment
56215709
[`Buffer.concat()`]: #static-method-bufferconcatlist-totallength
56225710
[`Buffer.copyBytesFrom()`]: #static-method-buffercopybytesfromview-offset-length
56235711
[`Buffer.from(array)`]: #static-method-bufferfromarray
@@ -5639,6 +5727,7 @@ introducing security vulnerabilities into an application.
56395727
[`TypedArray.prototype.subarray()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/subarray
56405728
[`blob.stream()`]: #blobstream
56415729
[`buf.buffer`]: #bufbuffer
5730+
[`buf.byteOffset`]: #bufbyteoffset
56425731
[`buf.compare()`]: #bufcomparetarget-targetstart-targetend-sourcestart-sourceend
56435732
[`buf.entries()`]: #bufentries
56445733
[`buf.fill()`]: #buffillvalue-offset-end-encoding
@@ -5653,6 +5742,7 @@ introducing security vulnerabilities into an application.
56535742
[`buffer.constants.MAX_STRING_LENGTH`]: #bufferconstantsmax_string_length
56545743
[`buffer.kMaxLength`]: #bufferkmaxlength
56555744
[`util.inspect()`]: util.md#utilinspectobject-options
5745+
[`v8.startupSnapshot.setDeserializeMainFunction()`]: v8.md#v8startupsnapshotsetdeserializemainfunctioncallback-data
56565746
[`v8::Uint8Array::kMaxLength`]: https://v8.github.io/api/head/classv8_1_1Uint8Array.html#a7677e3d0c9c92e4d40bef7212f5980c6
56575747
[base64url]: https://tools.ietf.org/html/rfc4648#section-5
56585748
[endianness]: https://en.wikipedia.org/wiki/Endianness

β€Ždoc/api/deprecations.mdβ€Ž

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4624,7 +4624,7 @@ will throw an error in a future version.
46244624
[`--pending-deprecation`]: cli.md#--pending-deprecation
46254625
[`--throw-deprecation`]: cli.md#--throw-deprecation
46264626
[`--unhandled-rejections`]: cli.md#--unhandled-rejectionsmode
4627-
[`Buffer.allocUnsafeSlow(size)`]: buffer.md#static-method-bufferallocunsafeslowsize
4627+
[`Buffer.allocUnsafeSlow(size)`]: buffer.md#static-method-bufferallocunsafeslowsize-alignment
46284628
[`Buffer.from(array)`]: buffer.md#static-method-bufferfromarray
46294629
[`Buffer.from(buffer)`]: buffer.md#static-method-bufferfrombuffer
46304630
[`Buffer.isBuffer()`]: buffer.md#static-method-bufferisbufferobj
@@ -4770,7 +4770,7 @@ will throw an error in a future version.
47704770
[`writable.writableLength`]: stream.md#writablewritablelength
47714771
[`zlib.bytesWritten`]: zlib.md#zlibbyteswritten
47724772
[alloc]: buffer.md#static-method-bufferallocsize-fill-encoding
4773-
[alloc_unsafe_size]: buffer.md#static-method-bufferallocunsafesize
4773+
[alloc_unsafe_size]: buffer.md#static-method-bufferallocunsafesize-alignment
47744774
[caveats of asynchronous customization hooks]: module.md#caveats-of-asynchronous-customization-hooks
47754775
[from_arraybuffer]: buffer.md#static-method-bufferfromarraybuffer-byteoffset-length
47764776
[from_string_encoding]: buffer.md#static-method-bufferfromstring-encoding

β€Ždoc/api/worker_threads.mdβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2234,7 +2234,7 @@ thread spawned will spawn another until the application crashes.
22342234
[`--max-old-space-size`]:cli.md#--max-old-space-sizesize-in-mib
22352235
[`--max-semi-space-size`]:cli.md#--max-semi-space-sizesize-in-mib
22362236
[`AsyncResource`]:async_hooks.md#class-asyncresource
2237-
[`Buffer.allocUnsafe()`]:buffer.md#static-method-bufferallocunsafesize
2237+
[`Buffer.allocUnsafe()`]:buffer.md#static-method-bufferallocunsafesize-alignment
22382238
[`ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST`]:errors.md#err_missing_message_port_in_transfer_list
22392239
[`ERR_WORKER_MESSAGING_ERRORED`]:errors.md#err_worker_messaging_errored
22402240
[`ERR_WORKER_MESSAGING_FAILED`]:errors.md#err_worker_messaging_failed

β€Žlib/buffer.jsβ€Ž

Lines changed: 77 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ const {
140140
markAsUntransferable,
141141
addBufferPrototypeMethods,
142142
createUnsafeBuffer,
143+
createUnsafeAlignedBuffer,
143144
asciiWrite,
144145
latin1Write,
145146
utf8Write,
@@ -171,13 +172,27 @@ const constants = ObjectDefineProperties({}, {
171172
},
172173
});
173174

175+
// The largest alignment accepted by `Buffer.allocUnsafeSlow()`. Any plausible
176+
// I/O alignment requirement (logical block size, memory page size, huge page
177+
// size) is well below this.
178+
constkMaxAlignment=2**30;
179+
180+
// Slices handed out of the pool are 8 byte aligned relative to the start of the
181+
// pool, so aligning the pool itself to a cache line keeps them from straddling
182+
// one more cache line than their size requires.
183+
constkPoolAlignment=64;
184+
174185
Buffer.poolSize=64*1024;
175-
letpoolSize,poolOffset,allocPool,allocBuffer;
186+
// `poolOffset` is relative to `poolBase`, which is where the pool starts inside
187+
// `allocPool`. The pool is over-allocated to be able to align it, so `poolBase`
188+
// is not necessarily 0.
189+
letpoolSize,poolOffset,poolBase,allocPool,allocBuffer;
176190

177191
functioncreatePool(){
178192
poolSize=Buffer.poolSize;
179-
allocBuffer=createUnsafeBuffer(poolSize);
193+
allocBuffer=createUnsafeAlignedBuffer(poolSize,kPoolAlignment);
180194
allocPool=allocBuffer.buffer;
195+
poolBase=TypedArrayPrototypeGetByteOffset(allocBuffer);
181196
markAsUntransferable(allocPool);
182197
poolOffset=0;
183198
}
@@ -444,40 +459,92 @@ Buffer.alloc = function alloc(size, fill, encoding) {
444459
/**
445460
* Equivalent to Buffer(num), by default creates a non-zero-filled Buffer
446461
* instance. If `--zero-fill-buffers` is set, will zero-fill the buffer.
462+
*
463+
* If `alignment` is given, the memory backing the returned buffer starts at an
464+
* address that is a multiple of `alignment`. See `Buffer.allocUnsafeSlow()`.
465+
* @param {number} size
466+
* @param {number} [alignment] A power of two, at most 2 ** 30
447467
* @returns {FastBuffer}
448468
*/
449-
Buffer.allocUnsafe=functionallocUnsafe(size){
469+
Buffer.allocUnsafe=functionallocUnsafe(size,alignment){
450470
validateNumber(size,'size',0,kMaxLength);
451-
returnallocate(size);
471+
if(alignment===undefined){
472+
returnallocate(size);
473+
}
474+
validateAlignment(size,alignment);
475+
returnallocateAligned(size,alignment);
452476
};
453477

454478
/**
455479
* By default creates a non-zero-filled Buffer instance that is not allocated
456480
* off the pre-initialized pool. If `--zero-fill-buffers` is set, will zero-fill
457481
* the buffer.
482+
*
483+
* If `alignment` is given, the memory backing the returned buffer starts at an
484+
* address that is a multiple of `alignment`, which is required by e.g. reads
485+
* and writes on file descriptors opened with `O_DIRECT`. Note that up to
486+
* `alignment - 1` extra bytes are allocated to satisfy the request, and that
487+
* the returned buffer's `byteOffset` is therefore usually non-zero.
458488
* @param {number} size
459-
* @returns {FastBuffer|undefined}
489+
* @param {number} [alignment] A power of two, at most 2 ** 30
490+
* @returns {FastBuffer}
460491
*/
461-
Buffer.allocUnsafeSlow=functionallocUnsafeSlow(size){
492+
Buffer.allocUnsafeSlow=functionallocUnsafeSlow(size,alignment){
462493
validateNumber(size,'size',0,kMaxLength);
463-
returncreateUnsafeBuffer(size);
494+
if(alignment===undefined){
495+
returncreateUnsafeBuffer(size);
496+
}
497+
validateAlignment(size,alignment);
498+
returncreateUnsafeAlignedBuffer(size,alignment);
464499
};
465500

501+
functionvalidateAlignment(size,alignment){
502+
validateInteger(alignment,'alignment',1,kMaxAlignment);
503+
if((alignment&(alignment-1))!==0){
504+
thrownewERR_INVALID_ARG_VALUE(
505+
'alignment',alignment,'must be a power of two');
506+
}
507+
// Satisfying the alignment costs up to `alignment - 1` extra bytes.
508+
if(size>kMaxLength-(alignment-1)){
509+
thrownewERR_OUT_OF_RANGE(
510+
'size',`<= ${kMaxLength-(alignment-1)}`,size);
511+
}
512+
}
513+
466514
functionallocate(size){
467515
if(size<=0){
468516
returnnewFastBuffer();
469517
}
470518
if(size<(Buffer.poolSize>>>1)){
471519
if(size>(poolSize-poolOffset))
472520
createPool();
473-
constb=newFastBuffer(allocPool,poolOffset,size);
521+
constb=newFastBuffer(allocPool,poolBase+poolOffset,size);
474522
poolOffset+=size;
475523
alignPool();
476524
returnb;
477525
}
478526
returncreateUnsafeBuffer(size);
479527
}
480528

529+
functionallocateAligned(size,alignment){
530+
if(size<=0){
531+
returnnewFastBuffer();
532+
}
533+
// The pool starts at a `kPoolAlignment` aligned address, so any alignment up
534+
// to that can be satisfied by padding the offset into the pool. Stricter
535+
// alignments need an allocation of their own.
536+
if(alignment>kPoolAlignment||size>=(Buffer.poolSize>>>1)){
537+
returncreateUnsafeAlignedBuffer(size,alignment);
538+
}
539+
poolOffset=(poolOffset+alignment-1)&~(alignment-1);
540+
if(size>(poolSize-poolOffset))
541+
createPool();
542+
constb=newFastBuffer(allocPool,poolBase+poolOffset,size);
543+
poolOffset+=size;
544+
alignPool();
545+
returnb;
546+
}
547+
481548
functionfromStringFast(string,ops){
482549
constmaxLength=Buffer.poolSize>>>1;
483550

@@ -498,7 +565,7 @@ function fromStringFast(string, ops) {
498565
createPool();
499566

500567
constactual=ops.write(allocBuffer,string,poolOffset,length);
501-
constb=newFastBuffer(allocPool,poolOffset,actual);
568+
constb=newFastBuffer(allocPool,poolBase+poolOffset,actual);
502569

503570
poolOffset+=actual;
504571
alignPool();
@@ -560,7 +627,7 @@ function fromArrayLike(obj) {
560627
if(length<(Buffer.poolSize>>>1)){
561628
if(length>(poolSize-poolOffset))
562629
createPool();
563-
constb=newFastBuffer(allocPool,poolOffset,length);
630+
constb=newFastBuffer(allocPool,poolBase+poolOffset,length);
564631
TypedArrayPrototypeSet(b,obj,0);
565632
poolOffset+=length;
566633
alignPool();

β€Žlib/internal/buffer.jsβ€Ž

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const {
3333
hexWrite,
3434
ucs2Write,
3535
utf8WriteStatic,
36+
arrayBufferAlignedOffset,
3637
createUnsafeArrayBuffer,
3738
setDetachKey,
3839
}=internalBinding('buffer');
@@ -1104,12 +1105,28 @@ function createUnsafeBuffer(size) {
11041105
returnnewFastBuffer(createUnsafeArrayBuffer(size));
11051106
}
11061107

1108+
// Returns an uninitialized buffer of `size` bytes whose first byte is located at
1109+
// a memory address that is a multiple of `alignment`. `alignment` must be a
1110+
// power of two, and `size + alignment - 1` must not exceed the maximum buffer
1111+
// length. Since the address of a backing store cannot be chosen, `alignment - 1`
1112+
// extra bytes are allocated and skipped, which leaves the returned buffer with a
1113+
// non-zero `byteOffset` into a larger ArrayBuffer.
1114+
functioncreateUnsafeAlignedBuffer(size,alignment){
1115+
if(size===0){
1116+
returnnewFastBuffer();
1117+
}
1118+
1119+
constab=createUnsafeArrayBuffer(size+alignment-1);
1120+
returnnewFastBuffer(ab,arrayBufferAlignedOffset(ab,alignment),size);
1121+
}
1122+
11071123
module.exports={
11081124
FastBuffer,
11091125
addBufferPrototypeMethods,
11101126
markAsUntransferable,
11111127
isMarkedAsUntransferable,
11121128
createUnsafeBuffer,
1129+
createUnsafeAlignedBuffer,
11131130
readUInt16BE,
11141131
readUInt32BE,
11151132
asciiWrite,

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit 615273d

Browse files
ronagclaude
authored andcommitted
buffer: support aligned allocations
Add an optional `alignment` argument to `Buffer.allocUnsafe()` and `Buffer.allocUnsafeSlow()`, which guarantees that the memory backing the returned buffer starts at an address that is a multiple of `alignment`. Some operating system interfaces refuse to work with unaligned memory. The motivating case is unbuffered ("direct") file I/O: a read or write on a descriptor opened with `O_DIRECT` fails with `EINVAL` unless the buffer address is a multiple of the logical block size of the underlying device. Until now there was no way to obtain such a buffer from JS, since the address of a backing store can neither be observed nor chosen. Alignment is also worth having purely for performance, for instance to keep a hot buffer from straddling one more cache line than its size requires. V8 does not allow picking the address of a backing store, so alignment is instead achieved by over-allocating `alignment - 1` bytes and positioning the buffer at the first suitably aligned byte within them. The new `arrayBufferAlignedOffset()` binding computes that offset. Addresses are not stable across snapshot serialization, so the binding reports no padding while a snapshot is being built. Otherwise the snapshot would capture where this particular process happened to allocate and stop being reproducible. Buffers restored from a snapshot are consequently not aligned; the pool works around this by recreating itself in a deserialize callback. The `Buffer.allocUnsafe()` pool is now aligned to a cache line itself, which lets pooled allocations satisfy any alignment up to 64 bytes by padding their offset into the pool rather than allocating separately. Assisted-by: Claude/Opus 5 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Robert Nagy <ronagy@icloud.com> PR-URL: #65003 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
1 parent 49fb028 commit 615273d

7 files changed

Lines changed: 415 additions & 35 deletions

File tree

β€Ždoc/api/buffer.mdβ€Ž

Lines changed: 94 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -791,11 +791,14 @@ data that might not have been allocated for `Buffer`s.
791791

792792
A `TypeError` will be thrown if `size` is not a number.
793793

794-
### Static method: `Buffer.allocUnsafe(size)`
794+
### Static method: `Buffer.allocUnsafe(size[, alignment])`
795795

796796
<!-- YAML
797797
added: v5.10.0
798798
changes:
799+
- version: REPLACEME
800+
pr-url: https://github.com/nodejs/node/pull/65003
801+
description: Added the `alignment` argument.
799802
- version: v20.0.0
800803
pr-url: https://github.com/nodejs/node/pull/45796
801804
description: Throw ERR_INVALID_ARG_TYPE or ERR_OUT_OF_RANGE instead of
@@ -810,6 +813,9 @@ changes:
810813
-->
811814

812815
*`size` {integer} The desired length of the new `Buffer`.
816+
*`alignment` {integer} If given, the memory backing the new `Buffer` will start
817+
at an address that is a multiple of `alignment`. Must be a power of two no
818+
larger than `2 ** 30`. See [Aligned allocations][].
813819
* Returns: {Buffer}
814820

815821
Allocates a new `Buffer` of `size` bytes. If `size` is larger than
@@ -865,11 +871,14 @@ pool, while `Buffer.allocUnsafe(size).fill(fill)` _will_ use the internal
865871
difference is subtle but can be important when an application requires the
866872
additional performance that [`Buffer.allocUnsafe()`][] provides.
867873

868-
### Static method: `Buffer.allocUnsafeSlow(size)`
874+
### Static method: `Buffer.allocUnsafeSlow(size[, alignment])`
869875

870876
<!-- YAML
871877
added: v5.12.0
872878
changes:
879+
- version: REPLACEME
880+
pr-url: https://github.com/nodejs/node/pull/65003
881+
description: Added the `alignment` argument.
873882
- version: v20.0.0
874883
pr-url: https://github.com/nodejs/node/pull/45796
875884
description: Throw ERR_INVALID_ARG_TYPE or ERR_OUT_OF_RANGE instead of
@@ -881,6 +890,9 @@ changes:
881890
-->
882891

883892
*`size` {integer} The desired length of the new `Buffer`.
893+
*`alignment` {integer} If given, the memory backing the new `Buffer` will start
894+
at an address that is a multiple of `alignment`. Must be a power of two no
895+
larger than `2 ** 30`. See [Aligned allocations][].
884896
* Returns: {Buffer}
885897

886898
Allocates a new `Buffer` of `size` bytes. If `size` is larger than
@@ -5608,16 +5620,92 @@ While there are clear performance advantages to using
56085620
[`Buffer.allocUnsafe()`][], extra care _must_ be taken in order to avoid
56095621
introducing security vulnerabilities into an application.
56105622

5623+
### Aligned allocations
5624+
5625+
Some operating system interfaces require the memory they operate on to be
5626+
aligned, and on some hardware alignment is merely faster. The most common
5627+
example of the former is unbuffered ("direct") file I/O, which on Linux requires
5628+
the buffer address, the file offset and the transfer length to all be multiples
5629+
of the logical block size of the underlying device:
5630+
5631+
```mjs
5632+
import { open } from'node:fs/promises';
5633+
import { constants } from'node:fs';
5634+
import { Buffer } from'node:buffer';
5635+
5636+
constblockSize=4096;
5637+
5638+
// The buffer address must be block-aligned for O_DIRECT to accept it.
5639+
constbuf=Buffer.allocUnsafeSlow(blockSize, blockSize);
5640+
5641+
constfile=awaitopen('/dev/sda', constants.O_RDONLY|constants.O_DIRECT);
5642+
try {
5643+
awaitfile.read(buf, 0, blockSize, 0);
5644+
} finally {
5645+
awaitfile.close();
5646+
}
5647+
```
5648+
5649+
```cjs
5650+
constfs=require('node:fs');
5651+
const { Buffer } =require('node:buffer');
5652+
5653+
constblockSize=4096;
5654+
5655+
// The buffer address must be block-aligned for O_DIRECT to accept it.
5656+
constbuf=Buffer.allocUnsafeSlow(blockSize, blockSize);
5657+
5658+
constflags=fs.constants.O_RDONLY|fs.constants.O_DIRECT;
5659+
fs.open('/dev/sda', flags, (err, fd) => {
5660+
if (err) throw err;
5661+
fs.read(fd, buf, 0, blockSize, 0, (err) => {
5662+
fs.close(fd, () => {});
5663+
if (err) throw err;
5664+
});
5665+
});
5666+
```
5667+
5668+
Alignment can also be worth requesting purely for performance, even when no
5669+
interface demands it. Aligning a hot `Buffer` to the cache line size (64 bytes on
5670+
most contemporary CPUs) keeps it from straddling one more cache line than it
5671+
needs to, so that a small structure is fetched with one cache miss instead of
5672+
two, and page-aligned (4096 bytes) allocations similarly help interfaces that map
5673+
or pin memory. These are micro-optimizations: measure before reaching for them,
5674+
since the extra bytes are not free.
5675+
5676+
Because the address of a `Buffer`'s memory cannot be chosen directly, extra bytes
5677+
have to be allocated or skipped to reach an aligned address.
5678+
[`Buffer.allocUnsafeSlow()`][] over-allocates up to `alignment - 1` bytes and
5679+
positions the returned `Buffer` at the first suitably aligned byte within them.
5680+
[`Buffer.allocUnsafe()`][] instead pads its offset into the shared internal pool,
5681+
whose start is always aligned to 64 bytes, and only falls back to an allocation
5682+
of its own when `alignment` is larger than that. Either way,
5683+
[`buf.byteOffset`][] is usually not 0 and [`buf.buffer`][] is larger than `size`,
5684+
so code that reaches past the `Buffer` into its underlying `ArrayBuffer` must
5685+
take the offset into account, as it must for pooled `Buffer`s.
5686+
5687+
The alignment is a property of the returned `Buffer` and is preserved for its
5688+
whole lifetime, but it is not inherited by other views: [`buf.subarray`][],
5689+
[`buf.slice()`][] and `structuredClone()` may all produce unaligned `Buffer`s.
5690+
5691+
Alignment also does not survive being captured in a startup snapshot: memory does
5692+
not keep its address across serialization, so a `Buffer` allocated while
5693+
[`--build-snapshot`][] is in effect is not aligned in the deserialized process.
5694+
Allocate inside a [`v8.startupSnapshot.setDeserializeMainFunction()`][] callback,
5695+
or after startup, if the alignment has to hold at run time.
5696+
56115697
[ASCII]: https://en.wikipedia.org/wiki/ASCII
5698+
[Aligned allocations]: #aligned-allocations
56125699
[Base64]: https://en.wikipedia.org/wiki/Base64
56135700
[ISO-8859-1]: https://en.wikipedia.org/wiki/ISO-8859-1
56145701
[RFC 4648, Section 5]: https://tools.ietf.org/html/rfc4648#section-5
56155702
[UTF-16]: https://en.wikipedia.org/wiki/UTF-16
56165703
[UTF-8]: https://en.wikipedia.org/wiki/UTF-8
56175704
[WHATWG Encoding Standard]: https://encoding.spec.whatwg.org/
5705+
[`--build-snapshot`]: cli.md#--build-snapshot
56185706
[`Buffer.alloc()`]: #static-method-bufferallocsize-fill-encoding
5619-
[`Buffer.allocUnsafe()`]: #static-method-bufferallocunsafesize
5620-
[`Buffer.allocUnsafeSlow()`]: #static-method-bufferallocunsafeslowsize
5707+
[`Buffer.allocUnsafe()`]: #static-method-bufferallocunsafesize-alignment
5708+
[`Buffer.allocUnsafeSlow()`]: #static-method-bufferallocunsafeslowsize-alignment
56215709
[`Buffer.concat()`]: #static-method-bufferconcatlist-totallength
56225710
[`Buffer.copyBytesFrom()`]: #static-method-buffercopybytesfromview-offset-length
56235711
[`Buffer.from(array)`]: #static-method-bufferfromarray
@@ -5639,6 +5727,7 @@ introducing security vulnerabilities into an application.
56395727
[`TypedArray.prototype.subarray()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/subarray
56405728
[`blob.stream()`]: #blobstream
56415729
[`buf.buffer`]: #bufbuffer
5730+
[`buf.byteOffset`]: #bufbyteoffset
56425731
[`buf.compare()`]: #bufcomparetarget-targetstart-targetend-sourcestart-sourceend
56435732
[`buf.entries()`]: #bufentries
56445733
[`buf.fill()`]: #buffillvalue-offset-end-encoding
@@ -5653,6 +5742,7 @@ introducing security vulnerabilities into an application.
56535742
[`buffer.constants.MAX_STRING_LENGTH`]: #bufferconstantsmax_string_length
56545743
[`buffer.kMaxLength`]: #bufferkmaxlength
56555744
[`util.inspect()`]: util.md#utilinspectobject-options
5745+
[`v8.startupSnapshot.setDeserializeMainFunction()`]: v8.md#v8startupsnapshotsetdeserializemainfunctioncallback-data
56565746
[`v8::Uint8Array::kMaxLength`]: https://v8.github.io/api/head/classv8_1_1Uint8Array.html#a7677e3d0c9c92e4d40bef7212f5980c6
56575747
[base64url]: https://tools.ietf.org/html/rfc4648#section-5
56585748
[endianness]: https://en.wikipedia.org/wiki/Endianness

β€Ždoc/api/deprecations.mdβ€Ž

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4624,7 +4624,7 @@ will throw an error in a future version.
46244624
[`--pending-deprecation`]: cli.md#--pending-deprecation
46254625
[`--throw-deprecation`]: cli.md#--throw-deprecation
46264626
[`--unhandled-rejections`]: cli.md#--unhandled-rejectionsmode
4627-
[`Buffer.allocUnsafeSlow(size)`]: buffer.md#static-method-bufferallocunsafeslowsize
4627+
[`Buffer.allocUnsafeSlow(size)`]: buffer.md#static-method-bufferallocunsafeslowsize-alignment
46284628
[`Buffer.from(array)`]: buffer.md#static-method-bufferfromarray
46294629
[`Buffer.from(buffer)`]: buffer.md#static-method-bufferfrombuffer
46304630
[`Buffer.isBuffer()`]: buffer.md#static-method-bufferisbufferobj
@@ -4770,7 +4770,7 @@ will throw an error in a future version.
47704770
[`writable.writableLength`]: stream.md#writablewritablelength
47714771
[`zlib.bytesWritten`]: zlib.md#zlibbyteswritten
47724772
[alloc]: buffer.md#static-method-bufferallocsize-fill-encoding
4773-
[alloc_unsafe_size]: buffer.md#static-method-bufferallocunsafesize
4773+
[alloc_unsafe_size]: buffer.md#static-method-bufferallocunsafesize-alignment
47744774
[caveats of asynchronous customization hooks]: module.md#caveats-of-asynchronous-customization-hooks
47754775
[from_arraybuffer]: buffer.md#static-method-bufferfromarraybuffer-byteoffset-length
47764776
[from_string_encoding]: buffer.md#static-method-bufferfromstring-encoding

β€Ždoc/api/worker_threads.mdβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2234,7 +2234,7 @@ thread spawned will spawn another until the application crashes.
22342234
[`--max-old-space-size`]:cli.md#--max-old-space-sizesize-in-mib
22352235
[`--max-semi-space-size`]:cli.md#--max-semi-space-sizesize-in-mib
22362236
[`AsyncResource`]:async_hooks.md#class-asyncresource
2237-
[`Buffer.allocUnsafe()`]:buffer.md#static-method-bufferallocunsafesize
2237+
[`Buffer.allocUnsafe()`]:buffer.md#static-method-bufferallocunsafesize-alignment
22382238
[`ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST`]:errors.md#err_missing_message_port_in_transfer_list
22392239
[`ERR_WORKER_MESSAGING_ERRORED`]:errors.md#err_worker_messaging_errored
22402240
[`ERR_WORKER_MESSAGING_FAILED`]:errors.md#err_worker_messaging_failed

β€Žlib/buffer.jsβ€Ž

Lines changed: 77 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ const {
140140
markAsUntransferable,
141141
addBufferPrototypeMethods,
142142
createUnsafeBuffer,
143+
createUnsafeAlignedBuffer,
143144
asciiWrite,
144145
latin1Write,
145146
utf8Write,
@@ -171,13 +172,27 @@ const constants = ObjectDefineProperties({}, {
171172
},
172173
});
173174

175+
// The largest alignment accepted by `Buffer.allocUnsafeSlow()`. Any plausible
176+
// I/O alignment requirement (logical block size, memory page size, huge page
177+
// size) is well below this.
178+
constkMaxAlignment=2**30;
179+
180+
// Slices handed out of the pool are 8 byte aligned relative to the start of the
181+
// pool, so aligning the pool itself to a cache line keeps them from straddling
182+
// one more cache line than their size requires.
183+
constkPoolAlignment=64;
184+
174185
Buffer.poolSize=64*1024;
175-
letpoolSize,poolOffset,allocPool,allocBuffer;
186+
// `poolOffset` is relative to `poolBase`, which is where the pool starts inside
187+
// `allocPool`. The pool is over-allocated to be able to align it, so `poolBase`
188+
// is not necessarily 0.
189+
letpoolSize,poolOffset,poolBase,allocPool,allocBuffer;
176190

177191
functioncreatePool(){
178192
poolSize=Buffer.poolSize;
179-
allocBuffer=createUnsafeBuffer(poolSize);
193+
allocBuffer=createUnsafeAlignedBuffer(poolSize,kPoolAlignment);
180194
allocPool=allocBuffer.buffer;
195+
poolBase=TypedArrayPrototypeGetByteOffset(allocBuffer);
181196
markAsUntransferable(allocPool);
182197
poolOffset=0;
183198
}
@@ -444,40 +459,92 @@ Buffer.alloc = function alloc(size, fill, encoding) {
444459
/**
445460
* Equivalent to Buffer(num), by default creates a non-zero-filled Buffer
446461
* instance. If `--zero-fill-buffers` is set, will zero-fill the buffer.
462+
*
463+
* If `alignment` is given, the memory backing the returned buffer starts at an
464+
* address that is a multiple of `alignment`. See `Buffer.allocUnsafeSlow()`.
465+
* @param {number} size
466+
* @param {number} [alignment] A power of two, at most 2 ** 30
447467
* @returns {FastBuffer}
448468
*/
449-
Buffer.allocUnsafe=functionallocUnsafe(size){
469+
Buffer.allocUnsafe=functionallocUnsafe(size,alignment){
450470
validateNumber(size,'size',0,kMaxLength);
451-
returnallocate(size);
471+
if(alignment===undefined){
472+
returnallocate(size);
473+
}
474+
validateAlignment(size,alignment);
475+
returnallocateAligned(size,alignment);
452476
};
453477

454478
/**
455479
* By default creates a non-zero-filled Buffer instance that is not allocated
456480
* off the pre-initialized pool. If `--zero-fill-buffers` is set, will zero-fill
457481
* the buffer.
482+
*
483+
* If `alignment` is given, the memory backing the returned buffer starts at an
484+
* address that is a multiple of `alignment`, which is required by e.g. reads
485+
* and writes on file descriptors opened with `O_DIRECT`. Note that up to
486+
* `alignment - 1` extra bytes are allocated to satisfy the request, and that
487+
* the returned buffer's `byteOffset` is therefore usually non-zero.
458488
* @param {number} size
459-
* @returns {FastBuffer|undefined}
489+
* @param {number} [alignment] A power of two, at most 2 ** 30
490+
* @returns {FastBuffer}
460491
*/
461-
Buffer.allocUnsafeSlow=functionallocUnsafeSlow(size){
492+
Buffer.allocUnsafeSlow=functionallocUnsafeSlow(size,alignment){
462493
validateNumber(size,'size',0,kMaxLength);
463-
returncreateUnsafeBuffer(size);
494+
if(alignment===undefined){
495+
returncreateUnsafeBuffer(size);
496+
}
497+
validateAlignment(size,alignment);
498+
returncreateUnsafeAlignedBuffer(size,alignment);
464499
};
465500

501+
functionvalidateAlignment(size,alignment){
502+
validateInteger(alignment,'alignment',1,kMaxAlignment);
503+
if((alignment&(alignment-1))!==0){
504+
thrownewERR_INVALID_ARG_VALUE(
505+
'alignment',alignment,'must be a power of two');
506+
}
507+
// Satisfying the alignment costs up to `alignment - 1` extra bytes.
508+
if(size>kMaxLength-(alignment-1)){
509+
thrownewERR_OUT_OF_RANGE(
510+
'size',`<= ${kMaxLength-(alignment-1)}`,size);
511+
}
512+
}
513+
466514
functionallocate(size){
467515
if(size<=0){
468516
returnnewFastBuffer();
469517
}
470518
if(size<(Buffer.poolSize>>>1)){
471519
if(size>(poolSize-poolOffset))
472520
createPool();
473-
constb=newFastBuffer(allocPool,poolOffset,size);
521+
constb=newFastBuffer(allocPool,poolBase+poolOffset,size);
474522
poolOffset+=size;
475523
alignPool();
476524
returnb;
477525
}
478526
returncreateUnsafeBuffer(size);
479527
}
480528

529+
functionallocateAligned(size,alignment){
530+
if(size<=0){
531+
returnnewFastBuffer();
532+
}
533+
// The pool starts at a `kPoolAlignment` aligned address, so any alignment up
534+
// to that can be satisfied by padding the offset into the pool. Stricter
535+
// alignments need an allocation of their own.
536+
if(alignment>kPoolAlignment||size>=(Buffer.poolSize>>>1)){
537+
returncreateUnsafeAlignedBuffer(size,alignment);
538+
}
539+
poolOffset=(poolOffset+alignment-1)&~(alignment-1);
540+
if(size>(poolSize-poolOffset))
541+
createPool();
542+
constb=newFastBuffer(allocPool,poolBase+poolOffset,size);
543+
poolOffset+=size;
544+
alignPool();
545+
returnb;
546+
}
547+
481548
functionfromStringFast(string,ops){
482549
constmaxLength=Buffer.poolSize>>>1;
483550

@@ -498,7 +565,7 @@ function fromStringFast(string, ops) {
498565
createPool();
499566

500567
constactual=ops.write(allocBuffer,string,poolOffset,length);
501-
constb=newFastBuffer(allocPool,poolOffset,actual);
568+
constb=newFastBuffer(allocPool,poolBase+poolOffset,actual);
502569

503570
poolOffset+=actual;
504571
alignPool();
@@ -560,7 +627,7 @@ function fromArrayLike(obj) {
560627
if(length<(Buffer.poolSize>>>1)){
561628
if(length>(poolSize-poolOffset))
562629
createPool();
563-
constb=newFastBuffer(allocPool,poolOffset,length);
630+
constb=newFastBuffer(allocPool,poolBase+poolOffset,length);
564631
TypedArrayPrototypeSet(b,obj,0);
565632
poolOffset+=length;
566633
alignPool();

β€Žlib/internal/buffer.jsβ€Ž

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const {
3333
hexWrite,
3434
ucs2Write,
3535
utf8WriteStatic,
36+
arrayBufferAlignedOffset,
3637
createUnsafeArrayBuffer,
3738
setDetachKey,
3839
}=internalBinding('buffer');
@@ -1104,12 +1105,28 @@ function createUnsafeBuffer(size) {
11041105
returnnewFastBuffer(createUnsafeArrayBuffer(size));
11051106
}
11061107

1108+
// Returns an uninitialized buffer of `size` bytes whose first byte is located at
1109+
// a memory address that is a multiple of `alignment`. `alignment` must be a
1110+
// power of two, and `size + alignment - 1` must not exceed the maximum buffer
1111+
// length. Since the address of a backing store cannot be chosen, `alignment - 1`
1112+
// extra bytes are allocated and skipped, which leaves the returned buffer with a
1113+
// non-zero `byteOffset` into a larger ArrayBuffer.
1114+
functioncreateUnsafeAlignedBuffer(size,alignment){
1115+
if(size===0){
1116+
returnnewFastBuffer();
1117+
}
1118+
1119+
constab=createUnsafeArrayBuffer(size+alignment-1);
1120+
returnnewFastBuffer(ab,arrayBufferAlignedOffset(ab,alignment),size);
1121+
}
1122+
11071123
module.exports={
11081124
FastBuffer,
11091125
addBufferPrototypeMethods,
11101126
markAsUntransferable,
11111127
isMarkedAsUntransferable,
11121128
createUnsafeBuffer,
1129+
createUnsafeAlignedBuffer,
11131130
readUInt16BE,
11141131
readUInt32BE,
11151132
asciiWrite,

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit 615273d

Browse files
ronagclaude
authored andcommitted
buffer: support aligned allocations
Add an optional `alignment` argument to `Buffer.allocUnsafe()` and `Buffer.allocUnsafeSlow()`, which guarantees that the memory backing the returned buffer starts at an address that is a multiple of `alignment`. Some operating system interfaces refuse to work with unaligned memory. The motivating case is unbuffered ("direct") file I/O: a read or write on a descriptor opened with `O_DIRECT` fails with `EINVAL` unless the buffer address is a multiple of the logical block size of the underlying device. Until now there was no way to obtain such a buffer from JS, since the address of a backing store can neither be observed nor chosen. Alignment is also worth having purely for performance, for instance to keep a hot buffer from straddling one more cache line than its size requires. V8 does not allow picking the address of a backing store, so alignment is instead achieved by over-allocating `alignment - 1` bytes and positioning the buffer at the first suitably aligned byte within them. The new `arrayBufferAlignedOffset()` binding computes that offset. Addresses are not stable across snapshot serialization, so the binding reports no padding while a snapshot is being built. Otherwise the snapshot would capture where this particular process happened to allocate and stop being reproducible. Buffers restored from a snapshot are consequently not aligned; the pool works around this by recreating itself in a deserialize callback. The `Buffer.allocUnsafe()` pool is now aligned to a cache line itself, which lets pooled allocations satisfy any alignment up to 64 bytes by padding their offset into the pool rather than allocating separately. Assisted-by: Claude/Opus 5 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Robert Nagy <ronagy@icloud.com> PR-URL: #65003 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
1 parent 49fb028 commit 615273d

7 files changed

Lines changed: 415 additions & 35 deletions

File tree

β€Ždoc/api/buffer.mdβ€Ž

Lines changed: 94 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -791,11 +791,14 @@ data that might not have been allocated for `Buffer`s.
791791

792792
A `TypeError` will be thrown if `size` is not a number.
793793

794-
### Static method: `Buffer.allocUnsafe(size)`
794+
### Static method: `Buffer.allocUnsafe(size[, alignment])`
795795

796796
<!-- YAML
797797
added: v5.10.0
798798
changes:
799+
- version: REPLACEME
800+
pr-url: https://github.com/nodejs/node/pull/65003
801+
description: Added the `alignment` argument.
799802
- version: v20.0.0
800803
pr-url: https://github.com/nodejs/node/pull/45796
801804
description: Throw ERR_INVALID_ARG_TYPE or ERR_OUT_OF_RANGE instead of
@@ -810,6 +813,9 @@ changes:
810813
-->
811814

812815
*`size` {integer} The desired length of the new `Buffer`.
816+
*`alignment` {integer} If given, the memory backing the new `Buffer` will start
817+
at an address that is a multiple of `alignment`. Must be a power of two no
818+
larger than `2 ** 30`. See [Aligned allocations][].
813819
* Returns: {Buffer}
814820

815821
Allocates a new `Buffer` of `size` bytes. If `size` is larger than
@@ -865,11 +871,14 @@ pool, while `Buffer.allocUnsafe(size).fill(fill)` _will_ use the internal
865871
difference is subtle but can be important when an application requires the
866872
additional performance that [`Buffer.allocUnsafe()`][] provides.
867873

868-
### Static method: `Buffer.allocUnsafeSlow(size)`
874+
### Static method: `Buffer.allocUnsafeSlow(size[, alignment])`
869875

870876
<!-- YAML
871877
added: v5.12.0
872878
changes:
879+
- version: REPLACEME
880+
pr-url: https://github.com/nodejs/node/pull/65003
881+
description: Added the `alignment` argument.
873882
- version: v20.0.0
874883
pr-url: https://github.com/nodejs/node/pull/45796
875884
description: Throw ERR_INVALID_ARG_TYPE or ERR_OUT_OF_RANGE instead of
@@ -881,6 +890,9 @@ changes:
881890
-->
882891

883892
*`size` {integer} The desired length of the new `Buffer`.
893+
*`alignment` {integer} If given, the memory backing the new `Buffer` will start
894+
at an address that is a multiple of `alignment`. Must be a power of two no
895+
larger than `2 ** 30`. See [Aligned allocations][].
884896
* Returns: {Buffer}
885897

886898
Allocates a new `Buffer` of `size` bytes. If `size` is larger than
@@ -5608,16 +5620,92 @@ While there are clear performance advantages to using
56085620
[`Buffer.allocUnsafe()`][], extra care _must_ be taken in order to avoid
56095621
introducing security vulnerabilities into an application.
56105622

5623+
### Aligned allocations
5624+
5625+
Some operating system interfaces require the memory they operate on to be
5626+
aligned, and on some hardware alignment is merely faster. The most common
5627+
example of the former is unbuffered ("direct") file I/O, which on Linux requires
5628+
the buffer address, the file offset and the transfer length to all be multiples
5629+
of the logical block size of the underlying device:
5630+
5631+
```mjs
5632+
import { open } from'node:fs/promises';
5633+
import { constants } from'node:fs';
5634+
import { Buffer } from'node:buffer';
5635+
5636+
constblockSize=4096;
5637+
5638+
// The buffer address must be block-aligned for O_DIRECT to accept it.
5639+
constbuf=Buffer.allocUnsafeSlow(blockSize, blockSize);
5640+
5641+
constfile=awaitopen('/dev/sda', constants.O_RDONLY|constants.O_DIRECT);
5642+
try {
5643+
awaitfile.read(buf, 0, blockSize, 0);
5644+
} finally {
5645+
awaitfile.close();
5646+
}
5647+
```
5648+
5649+
```cjs
5650+
constfs=require('node:fs');
5651+
const { Buffer } =require('node:buffer');
5652+
5653+
constblockSize=4096;
5654+
5655+
// The buffer address must be block-aligned for O_DIRECT to accept it.
5656+
constbuf=Buffer.allocUnsafeSlow(blockSize, blockSize);
5657+
5658+
constflags=fs.constants.O_RDONLY|fs.constants.O_DIRECT;
5659+
fs.open('/dev/sda', flags, (err, fd) => {
5660+
if (err) throw err;
5661+
fs.read(fd, buf, 0, blockSize, 0, (err) => {
5662+
fs.close(fd, () => {});
5663+
if (err) throw err;
5664+
});
5665+
});
5666+
```
5667+
5668+
Alignment can also be worth requesting purely for performance, even when no
5669+
interface demands it. Aligning a hot `Buffer` to the cache line size (64 bytes on
5670+
most contemporary CPUs) keeps it from straddling one more cache line than it
5671+
needs to, so that a small structure is fetched with one cache miss instead of
5672+
two, and page-aligned (4096 bytes) allocations similarly help interfaces that map
5673+
or pin memory. These are micro-optimizations: measure before reaching for them,
5674+
since the extra bytes are not free.
5675+
5676+
Because the address of a `Buffer`'s memory cannot be chosen directly, extra bytes
5677+
have to be allocated or skipped to reach an aligned address.
5678+
[`Buffer.allocUnsafeSlow()`][] over-allocates up to `alignment - 1` bytes and
5679+
positions the returned `Buffer` at the first suitably aligned byte within them.
5680+
[`Buffer.allocUnsafe()`][] instead pads its offset into the shared internal pool,
5681+
whose start is always aligned to 64 bytes, and only falls back to an allocation
5682+
of its own when `alignment` is larger than that. Either way,
5683+
[`buf.byteOffset`][] is usually not 0 and [`buf.buffer`][] is larger than `size`,
5684+
so code that reaches past the `Buffer` into its underlying `ArrayBuffer` must
5685+
take the offset into account, as it must for pooled `Buffer`s.
5686+
5687+
The alignment is a property of the returned `Buffer` and is preserved for its
5688+
whole lifetime, but it is not inherited by other views: [`buf.subarray`][],
5689+
[`buf.slice()`][] and `structuredClone()` may all produce unaligned `Buffer`s.
5690+
5691+
Alignment also does not survive being captured in a startup snapshot: memory does
5692+
not keep its address across serialization, so a `Buffer` allocated while
5693+
[`--build-snapshot`][] is in effect is not aligned in the deserialized process.
5694+
Allocate inside a [`v8.startupSnapshot.setDeserializeMainFunction()`][] callback,
5695+
or after startup, if the alignment has to hold at run time.
5696+
56115697
[ASCII]: https://en.wikipedia.org/wiki/ASCII
5698+
[Aligned allocations]: #aligned-allocations
56125699
[Base64]: https://en.wikipedia.org/wiki/Base64
56135700
[ISO-8859-1]: https://en.wikipedia.org/wiki/ISO-8859-1
56145701
[RFC 4648, Section 5]: https://tools.ietf.org/html/rfc4648#section-5
56155702
[UTF-16]: https://en.wikipedia.org/wiki/UTF-16
56165703
[UTF-8]: https://en.wikipedia.org/wiki/UTF-8
56175704
[WHATWG Encoding Standard]: https://encoding.spec.whatwg.org/
5705+
[`--build-snapshot`]: cli.md#--build-snapshot
56185706
[`Buffer.alloc()`]: #static-method-bufferallocsize-fill-encoding
5619-
[`Buffer.allocUnsafe()`]: #static-method-bufferallocunsafesize
5620-
[`Buffer.allocUnsafeSlow()`]: #static-method-bufferallocunsafeslowsize
5707+
[`Buffer.allocUnsafe()`]: #static-method-bufferallocunsafesize-alignment
5708+
[`Buffer.allocUnsafeSlow()`]: #static-method-bufferallocunsafeslowsize-alignment
56215709
[`Buffer.concat()`]: #static-method-bufferconcatlist-totallength
56225710
[`Buffer.copyBytesFrom()`]: #static-method-buffercopybytesfromview-offset-length
56235711
[`Buffer.from(array)`]: #static-method-bufferfromarray
@@ -5639,6 +5727,7 @@ introducing security vulnerabilities into an application.
56395727
[`TypedArray.prototype.subarray()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/subarray
56405728
[`blob.stream()`]: #blobstream
56415729
[`buf.buffer`]: #bufbuffer
5730+
[`buf.byteOffset`]: #bufbyteoffset
56425731
[`buf.compare()`]: #bufcomparetarget-targetstart-targetend-sourcestart-sourceend
56435732
[`buf.entries()`]: #bufentries
56445733
[`buf.fill()`]: #buffillvalue-offset-end-encoding
@@ -5653,6 +5742,7 @@ introducing security vulnerabilities into an application.
56535742
[`buffer.constants.MAX_STRING_LENGTH`]: #bufferconstantsmax_string_length
56545743
[`buffer.kMaxLength`]: #bufferkmaxlength
56555744
[`util.inspect()`]: util.md#utilinspectobject-options
5745+
[`v8.startupSnapshot.setDeserializeMainFunction()`]: v8.md#v8startupsnapshotsetdeserializemainfunctioncallback-data
56565746
[`v8::Uint8Array::kMaxLength`]: https://v8.github.io/api/head/classv8_1_1Uint8Array.html#a7677e3d0c9c92e4d40bef7212f5980c6
56575747
[base64url]: https://tools.ietf.org/html/rfc4648#section-5
56585748
[endianness]: https://en.wikipedia.org/wiki/Endianness

β€Ždoc/api/deprecations.mdβ€Ž

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4624,7 +4624,7 @@ will throw an error in a future version.
46244624
[`--pending-deprecation`]: cli.md#--pending-deprecation
46254625
[`--throw-deprecation`]: cli.md#--throw-deprecation
46264626
[`--unhandled-rejections`]: cli.md#--unhandled-rejectionsmode
4627-
[`Buffer.allocUnsafeSlow(size)`]: buffer.md#static-method-bufferallocunsafeslowsize
4627+
[`Buffer.allocUnsafeSlow(size)`]: buffer.md#static-method-bufferallocunsafeslowsize-alignment
46284628
[`Buffer.from(array)`]: buffer.md#static-method-bufferfromarray
46294629
[`Buffer.from(buffer)`]: buffer.md#static-method-bufferfrombuffer
46304630
[`Buffer.isBuffer()`]: buffer.md#static-method-bufferisbufferobj
@@ -4770,7 +4770,7 @@ will throw an error in a future version.
47704770
[`writable.writableLength`]: stream.md#writablewritablelength
47714771
[`zlib.bytesWritten`]: zlib.md#zlibbyteswritten
47724772
[alloc]: buffer.md#static-method-bufferallocsize-fill-encoding
4773-
[alloc_unsafe_size]: buffer.md#static-method-bufferallocunsafesize
4773+
[alloc_unsafe_size]: buffer.md#static-method-bufferallocunsafesize-alignment
47744774
[caveats of asynchronous customization hooks]: module.md#caveats-of-asynchronous-customization-hooks
47754775
[from_arraybuffer]: buffer.md#static-method-bufferfromarraybuffer-byteoffset-length
47764776
[from_string_encoding]: buffer.md#static-method-bufferfromstring-encoding

β€Ždoc/api/worker_threads.mdβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2234,7 +2234,7 @@ thread spawned will spawn another until the application crashes.
22342234
[`--max-old-space-size`]:cli.md#--max-old-space-sizesize-in-mib
22352235
[`--max-semi-space-size`]:cli.md#--max-semi-space-sizesize-in-mib
22362236
[`AsyncResource`]:async_hooks.md#class-asyncresource
2237-
[`Buffer.allocUnsafe()`]:buffer.md#static-method-bufferallocunsafesize
2237+
[`Buffer.allocUnsafe()`]:buffer.md#static-method-bufferallocunsafesize-alignment
22382238
[`ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST`]:errors.md#err_missing_message_port_in_transfer_list
22392239
[`ERR_WORKER_MESSAGING_ERRORED`]:errors.md#err_worker_messaging_errored
22402240
[`ERR_WORKER_MESSAGING_FAILED`]:errors.md#err_worker_messaging_failed

β€Žlib/buffer.jsβ€Ž

Lines changed: 77 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ const {
140140
markAsUntransferable,
141141
addBufferPrototypeMethods,
142142
createUnsafeBuffer,
143+
createUnsafeAlignedBuffer,
143144
asciiWrite,
144145
latin1Write,
145146
utf8Write,
@@ -171,13 +172,27 @@ const constants = ObjectDefineProperties({}, {
171172
},
172173
});
173174

175+
// The largest alignment accepted by `Buffer.allocUnsafeSlow()`. Any plausible
176+
// I/O alignment requirement (logical block size, memory page size, huge page
177+
// size) is well below this.
178+
constkMaxAlignment=2**30;
179+
180+
// Slices handed out of the pool are 8 byte aligned relative to the start of the
181+
// pool, so aligning the pool itself to a cache line keeps them from straddling
182+
// one more cache line than their size requires.
183+
constkPoolAlignment=64;
184+
174185
Buffer.poolSize=64*1024;
175-
letpoolSize,poolOffset,allocPool,allocBuffer;
186+
// `poolOffset` is relative to `poolBase`, which is where the pool starts inside
187+
// `allocPool`. The pool is over-allocated to be able to align it, so `poolBase`
188+
// is not necessarily 0.
189+
letpoolSize,poolOffset,poolBase,allocPool,allocBuffer;
176190

177191
functioncreatePool(){
178192
poolSize=Buffer.poolSize;
179-
allocBuffer=createUnsafeBuffer(poolSize);
193+
allocBuffer=createUnsafeAlignedBuffer(poolSize,kPoolAlignment);
180194
allocPool=allocBuffer.buffer;
195+
poolBase=TypedArrayPrototypeGetByteOffset(allocBuffer);
181196
markAsUntransferable(allocPool);
182197
poolOffset=0;
183198
}
@@ -444,40 +459,92 @@ Buffer.alloc = function alloc(size, fill, encoding) {
444459
/**
445460
* Equivalent to Buffer(num), by default creates a non-zero-filled Buffer
446461
* instance. If `--zero-fill-buffers` is set, will zero-fill the buffer.
462+
*
463+
* If `alignment` is given, the memory backing the returned buffer starts at an
464+
* address that is a multiple of `alignment`. See `Buffer.allocUnsafeSlow()`.
465+
* @param {number} size
466+
* @param {number} [alignment] A power of two, at most 2 ** 30
447467
* @returns {FastBuffer}
448468
*/
449-
Buffer.allocUnsafe=functionallocUnsafe(size){
469+
Buffer.allocUnsafe=functionallocUnsafe(size,alignment){
450470
validateNumber(size,'size',0,kMaxLength);
451-
returnallocate(size);
471+
if(alignment===undefined){
472+
returnallocate(size);
473+
}
474+
validateAlignment(size,alignment);
475+
returnallocateAligned(size,alignment);
452476
};
453477

454478
/**
455479
* By default creates a non-zero-filled Buffer instance that is not allocated
456480
* off the pre-initialized pool. If `--zero-fill-buffers` is set, will zero-fill
457481
* the buffer.
482+
*
483+
* If `alignment` is given, the memory backing the returned buffer starts at an
484+
* address that is a multiple of `alignment`, which is required by e.g. reads
485+
* and writes on file descriptors opened with `O_DIRECT`. Note that up to
486+
* `alignment - 1` extra bytes are allocated to satisfy the request, and that
487+
* the returned buffer's `byteOffset` is therefore usually non-zero.
458488
* @param {number} size
459-
* @returns {FastBuffer|undefined}
489+
* @param {number} [alignment] A power of two, at most 2 ** 30
490+
* @returns {FastBuffer}
460491
*/
461-
Buffer.allocUnsafeSlow=functionallocUnsafeSlow(size){
492+
Buffer.allocUnsafeSlow=functionallocUnsafeSlow(size,alignment){
462493
validateNumber(size,'size',0,kMaxLength);
463-
returncreateUnsafeBuffer(size);
494+
if(alignment===undefined){
495+
returncreateUnsafeBuffer(size);
496+
}
497+
validateAlignment(size,alignment);
498+
returncreateUnsafeAlignedBuffer(size,alignment);
464499
};
465500

501+
functionvalidateAlignment(size,alignment){
502+
validateInteger(alignment,'alignment',1,kMaxAlignment);
503+
if((alignment&(alignment-1))!==0){
504+
thrownewERR_INVALID_ARG_VALUE(
505+
'alignment',alignment,'must be a power of two');
506+
}
507+
// Satisfying the alignment costs up to `alignment - 1` extra bytes.
508+
if(size>kMaxLength-(alignment-1)){
509+
thrownewERR_OUT_OF_RANGE(
510+
'size',`<= ${kMaxLength-(alignment-1)}`,size);
511+
}
512+
}
513+
466514
functionallocate(size){
467515
if(size<=0){
468516
returnnewFastBuffer();
469517
}
470518
if(size<(Buffer.poolSize>>>1)){
471519
if(size>(poolSize-poolOffset))
472520
createPool();
473-
constb=newFastBuffer(allocPool,poolOffset,size);
521+
constb=newFastBuffer(allocPool,poolBase+poolOffset,size);
474522
poolOffset+=size;
475523
alignPool();
476524
returnb;
477525
}
478526
returncreateUnsafeBuffer(size);
479527
}
480528

529+
functionallocateAligned(size,alignment){
530+
if(size<=0){
531+
returnnewFastBuffer();
532+
}
533+
// The pool starts at a `kPoolAlignment` aligned address, so any alignment up
534+
// to that can be satisfied by padding the offset into the pool. Stricter
535+
// alignments need an allocation of their own.
536+
if(alignment>kPoolAlignment||size>=(Buffer.poolSize>>>1)){
537+
returncreateUnsafeAlignedBuffer(size,alignment);
538+
}
539+
poolOffset=(poolOffset+alignment-1)&~(alignment-1);
540+
if(size>(poolSize-poolOffset))
541+
createPool();
542+
constb=newFastBuffer(allocPool,poolBase+poolOffset,size);
543+
poolOffset+=size;
544+
alignPool();
545+
returnb;
546+
}
547+
481548
functionfromStringFast(string,ops){
482549
constmaxLength=Buffer.poolSize>>>1;
483550

@@ -498,7 +565,7 @@ function fromStringFast(string, ops) {
498565
createPool();
499566

500567
constactual=ops.write(allocBuffer,string,poolOffset,length);
501-
constb=newFastBuffer(allocPool,poolOffset,actual);
568+
constb=newFastBuffer(allocPool,poolBase+poolOffset,actual);
502569

503570
poolOffset+=actual;
504571
alignPool();
@@ -560,7 +627,7 @@ function fromArrayLike(obj) {
560627
if(length<(Buffer.poolSize>>>1)){
561628
if(length>(poolSize-poolOffset))
562629
createPool();
563-
constb=newFastBuffer(allocPool,poolOffset,length);
630+
constb=newFastBuffer(allocPool,poolBase+poolOffset,length);
564631
TypedArrayPrototypeSet(b,obj,0);
565632
poolOffset+=length;
566633
alignPool();

β€Žlib/internal/buffer.jsβ€Ž

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const {
3333
hexWrite,
3434
ucs2Write,
3535
utf8WriteStatic,
36+
arrayBufferAlignedOffset,
3637
createUnsafeArrayBuffer,
3738
setDetachKey,
3839
}=internalBinding('buffer');
@@ -1104,12 +1105,28 @@ function createUnsafeBuffer(size) {
11041105
returnnewFastBuffer(createUnsafeArrayBuffer(size));
11051106
}
11061107

1108+
// Returns an uninitialized buffer of `size` bytes whose first byte is located at
1109+
// a memory address that is a multiple of `alignment`. `alignment` must be a
1110+
// power of two, and `size + alignment - 1` must not exceed the maximum buffer
1111+
// length. Since the address of a backing store cannot be chosen, `alignment - 1`
1112+
// extra bytes are allocated and skipped, which leaves the returned buffer with a
1113+
// non-zero `byteOffset` into a larger ArrayBuffer.
1114+
functioncreateUnsafeAlignedBuffer(size,alignment){
1115+
if(size===0){
1116+
returnnewFastBuffer();
1117+
}
1118+
1119+
constab=createUnsafeArrayBuffer(size+alignment-1);
1120+
returnnewFastBuffer(ab,arrayBufferAlignedOffset(ab,alignment),size);
1121+
}
1122+
11071123
module.exports={
11081124
FastBuffer,
11091125
addBufferPrototypeMethods,
11101126
markAsUntransferable,
11111127
isMarkedAsUntransferable,
11121128
createUnsafeBuffer,
1129+
createUnsafeAlignedBuffer,
11131130
readUInt16BE,
11141131
readUInt32BE,
11151132
asciiWrite,

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Commit 615273d

Browse files
ronagclaude
authored andcommitted
buffer: support aligned allocations
Add an optional `alignment` argument to `Buffer.allocUnsafe()` and `Buffer.allocUnsafeSlow()`, which guarantees that the memory backing the returned buffer starts at an address that is a multiple of `alignment`. Some operating system interfaces refuse to work with unaligned memory. The motivating case is unbuffered ("direct") file I/O: a read or write on a descriptor opened with `O_DIRECT` fails with `EINVAL` unless the buffer address is a multiple of the logical block size of the underlying device. Until now there was no way to obtain such a buffer from JS, since the address of a backing store can neither be observed nor chosen. Alignment is also worth having purely for performance, for instance to keep a hot buffer from straddling one more cache line than its size requires. V8 does not allow picking the address of a backing store, so alignment is instead achieved by over-allocating `alignment - 1` bytes and positioning the buffer at the first suitably aligned byte within them. The new `arrayBufferAlignedOffset()` binding computes that offset. Addresses are not stable across snapshot serialization, so the binding reports no padding while a snapshot is being built. Otherwise the snapshot would capture where this particular process happened to allocate and stop being reproducible. Buffers restored from a snapshot are consequently not aligned; the pool works around this by recreating itself in a deserialize callback. The `Buffer.allocUnsafe()` pool is now aligned to a cache line itself, which lets pooled allocations satisfy any alignment up to 64 bytes by padding their offset into the pool rather than allocating separately. Assisted-by: Claude/Opus 5 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Robert Nagy <ronagy@icloud.com> PR-URL: #65003 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
1 parent 49fb028 commit 615273d

7 files changed

Lines changed: 415 additions & 35 deletions

File tree

β€Ždoc/api/buffer.mdβ€Ž

Lines changed: 94 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -791,11 +791,14 @@ data that might not have been allocated for `Buffer`s.
791791

792792
A `TypeError` will be thrown if `size` is not a number.
793793

794-
### Static method: `Buffer.allocUnsafe(size)`
794+
### Static method: `Buffer.allocUnsafe(size[, alignment])`
795795

796796
<!-- YAML
797797
added: v5.10.0
798798
changes:
799+
- version: REPLACEME
800+
pr-url: https://github.com/nodejs/node/pull/65003
801+
description: Added the `alignment` argument.
799802
- version: v20.0.0
800803
pr-url: https://github.com/nodejs/node/pull/45796
801804
description: Throw ERR_INVALID_ARG_TYPE or ERR_OUT_OF_RANGE instead of
@@ -810,6 +813,9 @@ changes:
810813
-->
811814

812815
*`size` {integer} The desired length of the new `Buffer`.
816+
*`alignment` {integer} If given, the memory backing the new `Buffer` will start
817+
at an address that is a multiple of `alignment`. Must be a power of two no
818+
larger than `2 ** 30`. See [Aligned allocations][].
813819
* Returns: {Buffer}
814820

815821
Allocates a new `Buffer` of `size` bytes. If `size` is larger than
@@ -865,11 +871,14 @@ pool, while `Buffer.allocUnsafe(size).fill(fill)` _will_ use the internal
865871
difference is subtle but can be important when an application requires the
866872
additional performance that [`Buffer.allocUnsafe()`][] provides.
867873

868-
### Static method: `Buffer.allocUnsafeSlow(size)`
874+
### Static method: `Buffer.allocUnsafeSlow(size[, alignment])`
869875

870876
<!-- YAML
871877
added: v5.12.0
872878
changes:
879+
- version: REPLACEME
880+
pr-url: https://github.com/nodejs/node/pull/65003
881+
description: Added the `alignment` argument.
873882
- version: v20.0.0
874883
pr-url: https://github.com/nodejs/node/pull/45796
875884
description: Throw ERR_INVALID_ARG_TYPE or ERR_OUT_OF_RANGE instead of
@@ -881,6 +890,9 @@ changes:
881890
-->
882891

883892
*`size` {integer} The desired length of the new `Buffer`.
893+
*`alignment` {integer} If given, the memory backing the new `Buffer` will start
894+
at an address that is a multiple of `alignment`. Must be a power of two no
895+
larger than `2 ** 30`. See [Aligned allocations][].
884896
* Returns: {Buffer}
885897

886898
Allocates a new `Buffer` of `size` bytes. If `size` is larger than
@@ -5608,16 +5620,92 @@ While there are clear performance advantages to using
56085620
[`Buffer.allocUnsafe()`][], extra care _must_ be taken in order to avoid
56095621
introducing security vulnerabilities into an application.
56105622

5623+
### Aligned allocations
5624+
5625+
Some operating system interfaces require the memory they operate on to be
5626+
aligned, and on some hardware alignment is merely faster. The most common
5627+
example of the former is unbuffered ("direct") file I/O, which on Linux requires
5628+
the buffer address, the file offset and the transfer length to all be multiples
5629+
of the logical block size of the underlying device:
5630+
5631+
```mjs
5632+
import { open } from'node:fs/promises';
5633+
import { constants } from'node:fs';
5634+
import { Buffer } from'node:buffer';
5635+
5636+
constblockSize=4096;
5637+
5638+
// The buffer address must be block-aligned for O_DIRECT to accept it.
5639+
constbuf=Buffer.allocUnsafeSlow(blockSize, blockSize);
5640+
5641+
constfile=awaitopen('/dev/sda', constants.O_RDONLY|constants.O_DIRECT);
5642+
try {
5643+
awaitfile.read(buf, 0, blockSize, 0);
5644+
} finally {
5645+
awaitfile.close();
5646+
}
5647+
```
5648+
5649+
```cjs
5650+
constfs=require('node:fs');
5651+
const { Buffer } =require('node:buffer');
5652+
5653+
constblockSize=4096;
5654+
5655+
// The buffer address must be block-aligned for O_DIRECT to accept it.
5656+
constbuf=Buffer.allocUnsafeSlow(blockSize, blockSize);
5657+
5658+
constflags=fs.constants.O_RDONLY|fs.constants.O_DIRECT;
5659+
fs.open('/dev/sda', flags, (err, fd) => {
5660+
if (err) throw err;
5661+
fs.read(fd, buf, 0, blockSize, 0, (err) => {
5662+
fs.close(fd, () => {});
5663+
if (err) throw err;
5664+
});
5665+
});
5666+
```
5667+
5668+
Alignment can also be worth requesting purely for performance, even when no
5669+
interface demands it. Aligning a hot `Buffer` to the cache line size (64 bytes on
5670+
most contemporary CPUs) keeps it from straddling one more cache line than it
5671+
needs to, so that a small structure is fetched with one cache miss instead of
5672+
two, and page-aligned (4096 bytes) allocations similarly help interfaces that map
5673+
or pin memory. These are micro-optimizations: measure before reaching for them,
5674+
since the extra bytes are not free.
5675+
5676+
Because the address of a `Buffer`'s memory cannot be chosen directly, extra bytes
5677+
have to be allocated or skipped to reach an aligned address.
5678+
[`Buffer.allocUnsafeSlow()`][] over-allocates up to `alignment - 1` bytes and
5679+
positions the returned `Buffer` at the first suitably aligned byte within them.
5680+
[`Buffer.allocUnsafe()`][] instead pads its offset into the shared internal pool,
5681+
whose start is always aligned to 64 bytes, and only falls back to an allocation
5682+
of its own when `alignment` is larger than that. Either way,
5683+
[`buf.byteOffset`][] is usually not 0 and [`buf.buffer`][] is larger than `size`,
5684+
so code that reaches past the `Buffer` into its underlying `ArrayBuffer` must
5685+
take the offset into account, as it must for pooled `Buffer`s.
5686+
5687+
The alignment is a property of the returned `Buffer` and is preserved for its
5688+
whole lifetime, but it is not inherited by other views: [`buf.subarray`][],
5689+
[`buf.slice()`][] and `structuredClone()` may all produce unaligned `Buffer`s.
5690+
5691+
Alignment also does not survive being captured in a startup snapshot: memory does
5692+
not keep its address across serialization, so a `Buffer` allocated while
5693+
[`--build-snapshot`][] is in effect is not aligned in the deserialized process.
5694+
Allocate inside a [`v8.startupSnapshot.setDeserializeMainFunction()`][] callback,
5695+
or after startup, if the alignment has to hold at run time.
5696+
56115697
[ASCII]: https://en.wikipedia.org/wiki/ASCII
5698+
[Aligned allocations]: #aligned-allocations
56125699
[Base64]: https://en.wikipedia.org/wiki/Base64
56135700
[ISO-8859-1]: https://en.wikipedia.org/wiki/ISO-8859-1
56145701
[RFC 4648, Section 5]: https://tools.ietf.org/html/rfc4648#section-5
56155702
[UTF-16]: https://en.wikipedia.org/wiki/UTF-16
56165703
[UTF-8]: https://en.wikipedia.org/wiki/UTF-8
56175704
[WHATWG Encoding Standard]: https://encoding.spec.whatwg.org/
5705+
[`--build-snapshot`]: cli.md#--build-snapshot
56185706
[`Buffer.alloc()`]: #static-method-bufferallocsize-fill-encoding
5619-
[`Buffer.allocUnsafe()`]: #static-method-bufferallocunsafesize
5620-
[`Buffer.allocUnsafeSlow()`]: #static-method-bufferallocunsafeslowsize
5707+
[`Buffer.allocUnsafe()`]: #static-method-bufferallocunsafesize-alignment
5708+
[`Buffer.allocUnsafeSlow()`]: #static-method-bufferallocunsafeslowsize-alignment
56215709
[`Buffer.concat()`]: #static-method-bufferconcatlist-totallength
56225710
[`Buffer.copyBytesFrom()`]: #static-method-buffercopybytesfromview-offset-length
56235711
[`Buffer.from(array)`]: #static-method-bufferfromarray
@@ -5639,6 +5727,7 @@ introducing security vulnerabilities into an application.
56395727
[`TypedArray.prototype.subarray()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/subarray
56405728
[`blob.stream()`]: #blobstream
56415729
[`buf.buffer`]: #bufbuffer
5730+
[`buf.byteOffset`]: #bufbyteoffset
56425731
[`buf.compare()`]: #bufcomparetarget-targetstart-targetend-sourcestart-sourceend
56435732
[`buf.entries()`]: #bufentries
56445733
[`buf.fill()`]: #buffillvalue-offset-end-encoding
@@ -5653,6 +5742,7 @@ introducing security vulnerabilities into an application.
56535742
[`buffer.constants.MAX_STRING_LENGTH`]: #bufferconstantsmax_string_length
56545743
[`buffer.kMaxLength`]: #bufferkmaxlength
56555744
[`util.inspect()`]: util.md#utilinspectobject-options
5745+
[`v8.startupSnapshot.setDeserializeMainFunction()`]: v8.md#v8startupsnapshotsetdeserializemainfunctioncallback-data
56565746
[`v8::Uint8Array::kMaxLength`]: https://v8.github.io/api/head/classv8_1_1Uint8Array.html#a7677e3d0c9c92e4d40bef7212f5980c6
56575747
[base64url]: https://tools.ietf.org/html/rfc4648#section-5
56585748
[endianness]: https://en.wikipedia.org/wiki/Endianness

β€Ždoc/api/deprecations.mdβ€Ž

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4624,7 +4624,7 @@ will throw an error in a future version.
46244624
[`--pending-deprecation`]: cli.md#--pending-deprecation
46254625
[`--throw-deprecation`]: cli.md#--throw-deprecation
46264626
[`--unhandled-rejections`]: cli.md#--unhandled-rejectionsmode
4627-
[`Buffer.allocUnsafeSlow(size)`]: buffer.md#static-method-bufferallocunsafeslowsize
4627+
[`Buffer.allocUnsafeSlow(size)`]: buffer.md#static-method-bufferallocunsafeslowsize-alignment
46284628
[`Buffer.from(array)`]: buffer.md#static-method-bufferfromarray
46294629
[`Buffer.from(buffer)`]: buffer.md#static-method-bufferfrombuffer
46304630
[`Buffer.isBuffer()`]: buffer.md#static-method-bufferisbufferobj
@@ -4770,7 +4770,7 @@ will throw an error in a future version.
47704770
[`writable.writableLength`]: stream.md#writablewritablelength
47714771
[`zlib.bytesWritten`]: zlib.md#zlibbyteswritten
47724772
[alloc]: buffer.md#static-method-bufferallocsize-fill-encoding
4773-
[alloc_unsafe_size]: buffer.md#static-method-bufferallocunsafesize
4773+
[alloc_unsafe_size]: buffer.md#static-method-bufferallocunsafesize-alignment
47744774
[caveats of asynchronous customization hooks]: module.md#caveats-of-asynchronous-customization-hooks
47754775
[from_arraybuffer]: buffer.md#static-method-bufferfromarraybuffer-byteoffset-length
47764776
[from_string_encoding]: buffer.md#static-method-bufferfromstring-encoding

β€Ždoc/api/worker_threads.mdβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2234,7 +2234,7 @@ thread spawned will spawn another until the application crashes.
22342234
[`--max-old-space-size`]:cli.md#--max-old-space-sizesize-in-mib
22352235
[`--max-semi-space-size`]:cli.md#--max-semi-space-sizesize-in-mib
22362236
[`AsyncResource`]:async_hooks.md#class-asyncresource
2237-
[`Buffer.allocUnsafe()`]:buffer.md#static-method-bufferallocunsafesize
2237+
[`Buffer.allocUnsafe()`]:buffer.md#static-method-bufferallocunsafesize-alignment
22382238
[`ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST`]:errors.md#err_missing_message_port_in_transfer_list
22392239
[`ERR_WORKER_MESSAGING_ERRORED`]:errors.md#err_worker_messaging_errored
22402240
[`ERR_WORKER_MESSAGING_FAILED`]:errors.md#err_worker_messaging_failed

β€Žlib/buffer.jsβ€Ž

Lines changed: 77 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ const {
140140
markAsUntransferable,
141141
addBufferPrototypeMethods,
142142
createUnsafeBuffer,
143+
createUnsafeAlignedBuffer,
143144
asciiWrite,
144145
latin1Write,
145146
utf8Write,
@@ -171,13 +172,27 @@ const constants = ObjectDefineProperties({}, {
171172
},
172173
});
173174

175+
// The largest alignment accepted by `Buffer.allocUnsafeSlow()`. Any plausible
176+
// I/O alignment requirement (logical block size, memory page size, huge page
177+
// size) is well below this.
178+
constkMaxAlignment=2**30;
179+
180+
// Slices handed out of the pool are 8 byte aligned relative to the start of the
181+
// pool, so aligning the pool itself to a cache line keeps them from straddling
182+
// one more cache line than their size requires.
183+
constkPoolAlignment=64;
184+
174185
Buffer.poolSize=64*1024;
175-
letpoolSize,poolOffset,allocPool,allocBuffer;
186+
// `poolOffset` is relative to `poolBase`, which is where the pool starts inside
187+
// `allocPool`. The pool is over-allocated to be able to align it, so `poolBase`
188+
// is not necessarily 0.
189+
letpoolSize,poolOffset,poolBase,allocPool,allocBuffer;
176190

177191
functioncreatePool(){
178192
poolSize=Buffer.poolSize;
179-
allocBuffer=createUnsafeBuffer(poolSize);
193+
allocBuffer=createUnsafeAlignedBuffer(poolSize,kPoolAlignment);
180194
allocPool=allocBuffer.buffer;
195+
poolBase=TypedArrayPrototypeGetByteOffset(allocBuffer);
181196
markAsUntransferable(allocPool);
182197
poolOffset=0;
183198
}
@@ -444,40 +459,92 @@ Buffer.alloc = function alloc(size, fill, encoding) {
444459
/**
445460
* Equivalent to Buffer(num), by default creates a non-zero-filled Buffer
446461
* instance. If `--zero-fill-buffers` is set, will zero-fill the buffer.
462+
*
463+
* If `alignment` is given, the memory backing the returned buffer starts at an
464+
* address that is a multiple of `alignment`. See `Buffer.allocUnsafeSlow()`.
465+
* @param {number} size
466+
* @param {number} [alignment] A power of two, at most 2 ** 30
447467
* @returns {FastBuffer}
448468
*/
449-
Buffer.allocUnsafe=functionallocUnsafe(size){
469+
Buffer.allocUnsafe=functionallocUnsafe(size,alignment){
450470
validateNumber(size,'size',0,kMaxLength);
451-
returnallocate(size);
471+
if(alignment===undefined){
472+
returnallocate(size);
473+
}
474+
validateAlignment(size,alignment);
475+
returnallocateAligned(size,alignment);
452476
};
453477

454478
/**
455479
* By default creates a non-zero-filled Buffer instance that is not allocated
456480
* off the pre-initialized pool. If `--zero-fill-buffers` is set, will zero-fill
457481
* the buffer.
482+
*
483+
* If `alignment` is given, the memory backing the returned buffer starts at an
484+
* address that is a multiple of `alignment`, which is required by e.g. reads
485+
* and writes on file descriptors opened with `O_DIRECT`. Note that up to
486+
* `alignment - 1` extra bytes are allocated to satisfy the request, and that
487+
* the returned buffer's `byteOffset` is therefore usually non-zero.
458488
* @param {number} size
459-
* @returns {FastBuffer|undefined}
489+
* @param {number} [alignment] A power of two, at most 2 ** 30
490+
* @returns {FastBuffer}
460491
*/
461-
Buffer.allocUnsafeSlow=functionallocUnsafeSlow(size){
492+
Buffer.allocUnsafeSlow=functionallocUnsafeSlow(size,alignment){
462493
validateNumber(size,'size',0,kMaxLength);
463-
returncreateUnsafeBuffer(size);
494+
if(alignment===undefined){
495+
returncreateUnsafeBuffer(size);
496+
}
497+
validateAlignment(size,alignment);
498+
returncreateUnsafeAlignedBuffer(size,alignment);
464499
};
465500

501+
functionvalidateAlignment(size,alignment){
502+
validateInteger(alignment,'alignment',1,kMaxAlignment);
503+
if((alignment&(alignment-1))!==0){
504+
thrownewERR_INVALID_ARG_VALUE(
505+
'alignment',alignment,'must be a power of two');
506+
}
507+
// Satisfying the alignment costs up to `alignment - 1` extra bytes.
508+
if(size>kMaxLength-(alignment-1)){
509+
thrownewERR_OUT_OF_RANGE(
510+
'size',`<= ${kMaxLength-(alignment-1)}`,size);
511+
}
512+
}
513+
466514
functionallocate(size){
467515
if(size<=0){
468516
returnnewFastBuffer();
469517
}
470518
if(size<(Buffer.poolSize>>>1)){
471519
if(size>(poolSize-poolOffset))
472520
createPool();
473-
constb=newFastBuffer(allocPool,poolOffset,size);
521+
constb=newFastBuffer(allocPool,poolBase+poolOffset,size);
474522
poolOffset+=size;
475523
alignPool();
476524
returnb;
477525
}
478526
returncreateUnsafeBuffer(size);
479527
}
480528

529+
functionallocateAligned(size,alignment){
530+
if(size<=0){
531+
returnnewFastBuffer();
532+
}
533+
// The pool starts at a `kPoolAlignment` aligned address, so any alignment up
534+
// to that can be satisfied by padding the offset into the pool. Stricter
535+
// alignments need an allocation of their own.
536+
if(alignment>kPoolAlignment||size>=(Buffer.poolSize>>>1)){
537+
returncreateUnsafeAlignedBuffer(size,alignment);
538+
}
539+
poolOffset=(poolOffset+alignment-1)&~(alignment-1);
540+
if(size>(poolSize-poolOffset))
541+
createPool();
542+
constb=newFastBuffer(allocPool,poolBase+poolOffset,size);
543+
poolOffset+=size;
544+
alignPool();
545+
returnb;
546+
}
547+
481548
functionfromStringFast(string,ops){
482549
constmaxLength=Buffer.poolSize>>>1;
483550

@@ -498,7 +565,7 @@ function fromStringFast(string, ops) {
498565
createPool();
499566

500567
constactual=ops.write(allocBuffer,string,poolOffset,length);
501-
constb=newFastBuffer(allocPool,poolOffset,actual);
568+
constb=newFastBuffer(allocPool,poolBase+poolOffset,actual);
502569

503570
poolOffset+=actual;
504571
alignPool();
@@ -560,7 +627,7 @@ function fromArrayLike(obj) {
560627
if(length<(Buffer.poolSize>>>1)){
561628
if(length>(poolSize-poolOffset))
562629
createPool();
563-
constb=newFastBuffer(allocPool,poolOffset,length);
630+
constb=newFastBuffer(allocPool,poolBase+poolOffset,length);
564631
TypedArrayPrototypeSet(b,obj,0);
565632
poolOffset+=length;
566633
alignPool();

β€Žlib/internal/buffer.jsβ€Ž

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const {
3333
hexWrite,
3434
ucs2Write,
3535
utf8WriteStatic,
36+
arrayBufferAlignedOffset,
3637
createUnsafeArrayBuffer,
3738
setDetachKey,
3839
}=internalBinding('buffer');
@@ -1104,12 +1105,28 @@ function createUnsafeBuffer(size) {
11041105
returnnewFastBuffer(createUnsafeArrayBuffer(size));
11051106
}
11061107

1108+
// Returns an uninitialized buffer of `size` bytes whose first byte is located at
1109+
// a memory address that is a multiple of `alignment`. `alignment` must be a
1110+
// power of two, and `size + alignment - 1` must not exceed the maximum buffer
1111+
// length. Since the address of a backing store cannot be chosen, `alignment - 1`
1112+
// extra bytes are allocated and skipped, which leaves the returned buffer with a
1113+
// non-zero `byteOffset` into a larger ArrayBuffer.
1114+
functioncreateUnsafeAlignedBuffer(size,alignment){
1115+
if(size===0){
1116+
returnnewFastBuffer();
1117+
}
1118+
1119+
constab=createUnsafeArrayBuffer(size+alignment-1);
1120+
returnnewFastBuffer(ab,arrayBufferAlignedOffset(ab,alignment),size);
1121+
}
1122+
11071123
module.exports={
11081124
FastBuffer,
11091125
addBufferPrototypeMethods,
11101126
markAsUntransferable,
11111127
isMarkedAsUntransferable,
11121128
createUnsafeBuffer,
1129+
createUnsafeAlignedBuffer,
11131130
readUInt16BE,
11141131
readUInt32BE,
11151132
asciiWrite,

0 commit comments

Comments
Β (0)