Skip to content

Commit ef04e22

Browse files
ronagclaude
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. 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>
1 parent f00fb75 commit ef04e22

7 files changed

Lines changed: 375 additions & 35 deletions

File tree

doc/api/buffer.md

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

794794
A `TypeError` will be thrown if `size` is not a number.
795795

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

798798
<!-- YAML
799799
added: v5.10.0
800800
changes:
801+
- version: REPLACEME
802+
pr-url: https://github.com/nodejs/node/pull/65003
803+
description: Added the `alignment` argument.
801804
- version: v20.0.0
802805
pr-url: https://github.com/nodejs/node/pull/45796
803806
description: Throw ERR_INVALID_ARG_TYPE or ERR_OUT_OF_RANGE instead of
@@ -812,6 +815,9 @@ changes:
812815
-->
813816

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

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

870-
### Static method: `Buffer.allocUnsafeSlow(size)`
876+
### Static method: `Buffer.allocUnsafeSlow(size[, alignment])`
871877

872878
<!-- YAML
873879
added: v5.12.0
874880
changes:
881+
- version: REPLACEME
882+
pr-url: https://github.com/nodejs/node/pull/65003
883+
description: Added the `alignment` argument.
875884
- version: v20.0.0
876885
pr-url: https://github.com/nodejs/node/pull/45796
877886
description: Throw ERR_INVALID_ARG_TYPE or ERR_OUT_OF_RANGE instead of
@@ -883,6 +892,9 @@ changes:
883892
-->
884893

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

888900
Allocates a new `Buffer` of `size` bytes. If `size` is larger than
@@ -5602,16 +5614,85 @@ While there are clear performance advantages to using
56025614
[`Buffer.allocUnsafe()`][], extra care _must_ be taken in order to avoid
56035615
introducing security vulnerabilities into an application.
56045616

5617+
### Aligned allocations
5618+
5619+
Some operating system interfaces require the memory they operate on to be
5620+
aligned, and on some hardware alignment is merely faster. The most common
5621+
example of the former is unbuffered ("direct") file I/O, which on Linux requires
5622+
the buffer address, the file offset and the transfer length to all be multiples
5623+
of the logical block size of the underlying device:
5624+
5625+
```mjs
5626+
import { open } from 'node:fs/promises';
5627+
import { constants } from 'node:fs';
5628+
import { Buffer } from 'node:buffer';
5629+
5630+
const blockSize = 4096;
5631+
5632+
// The buffer address must be block-aligned for O_DIRECT to accept it.
5633+
const buf = Buffer.allocUnsafeSlow(blockSize, blockSize);
5634+
5635+
const file = await open('/dev/sda', constants.O_RDONLY | constants.O_DIRECT);
5636+
try {
5637+
await file.read(buf, 0, blockSize, 0);
5638+
} finally {
5639+
await file.close();
5640+
}
5641+
```
5642+
5643+
```cjs
5644+
const fs = require('node:fs');
5645+
const { Buffer } = require('node:buffer');
5646+
5647+
const blockSize = 4096;
5648+
5649+
// The buffer address must be block-aligned for O_DIRECT to accept it.
5650+
const buf = Buffer.allocUnsafeSlow(blockSize, blockSize);
5651+
5652+
const flags = fs.constants.O_RDONLY | fs.constants.O_DIRECT;
5653+
fs.open('/dev/sda', flags, (err, fd) => {
5654+
if (err) throw err;
5655+
fs.read(fd, buf, 0, blockSize, 0, (err) => {
5656+
fs.close(fd, () => {});
5657+
if (err) throw err;
5658+
});
5659+
});
5660+
```
5661+
5662+
Alignment can also be worth requesting purely for performance, even when no
5663+
interface demands it. Aligning a hot `Buffer` to the cache line size (64 bytes on
5664+
most contemporary CPUs) keeps it from straddling one more cache line than it
5665+
needs to, so that a small structure is fetched with one cache miss instead of
5666+
two, and page-aligned (4096 bytes) allocations similarly help interfaces that map
5667+
or pin memory. These are micro-optimizations: measure before reaching for them,
5668+
since the extra bytes are not free.
5669+
5670+
Because the address of a `Buffer`'s memory cannot be chosen directly, extra bytes
5671+
have to be allocated or skipped to reach an aligned address.
5672+
[`Buffer.allocUnsafeSlow()`][] over-allocates up to `alignment - 1` bytes and
5673+
positions the returned `Buffer` at the first suitably aligned byte within them.
5674+
[`Buffer.allocUnsafe()`][] instead pads its offset into the shared internal pool,
5675+
whose start is always aligned to 64 bytes, and only falls back to an allocation
5676+
of its own when `alignment` is larger than that. Either way,
5677+
[`buf.byteOffset`][] is usually not 0 and [`buf.buffer`][] is larger than `size`,
5678+
so code that reaches past the `Buffer` into its underlying `ArrayBuffer` must
5679+
take the offset into account, as it must for pooled `Buffer`s.
5680+
5681+
The alignment is a property of the returned `Buffer` and is preserved for its
5682+
whole lifetime, but it is not inherited by other views: [`buf.subarray`][],
5683+
[`buf.slice()`][] and `structuredClone()` may all produce unaligned `Buffer`s.
5684+
56055685
[ASCII]: https://en.wikipedia.org/wiki/ASCII
5686+
[Aligned allocations]: #aligned-allocations
56065687
[Base64]: https://en.wikipedia.org/wiki/Base64
56075688
[ISO-8859-1]: https://en.wikipedia.org/wiki/ISO-8859-1
56085689
[RFC 4648, Section 5]: https://tools.ietf.org/html/rfc4648#section-5
56095690
[UTF-16]: https://en.wikipedia.org/wiki/UTF-16
56105691
[UTF-8]: https://en.wikipedia.org/wiki/UTF-8
56115692
[WHATWG Encoding Standard]: https://encoding.spec.whatwg.org/
56125693
[`Buffer.alloc()`]: #static-method-bufferallocsize-fill-encoding
5613-
[`Buffer.allocUnsafe()`]: #static-method-bufferallocunsafesize
5614-
[`Buffer.allocUnsafeSlow()`]: #static-method-bufferallocunsafeslowsize
5694+
[`Buffer.allocUnsafe()`]: #static-method-bufferallocunsafesize-alignment
5695+
[`Buffer.allocUnsafeSlow()`]: #static-method-bufferallocunsafeslowsize-alignment
56155696
[`Buffer.concat()`]: #static-method-bufferconcatlist-totallength
56165697
[`Buffer.copyBytesFrom()`]: #static-method-buffercopybytesfromview-offset-length
56175698
[`Buffer.from(array)`]: #static-method-bufferfromarray
@@ -5633,6 +5714,7 @@ introducing security vulnerabilities into an application.
56335714
[`TypedArray.prototype.subarray()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/subarray
56345715
[`blob.stream()`]: #blobstream
56355716
[`buf.buffer`]: #bufbuffer
5717+
[`buf.byteOffset`]: #bufbyteoffset
56365718
[`buf.compare()`]: #bufcomparetarget-targetstart-targetend-sourcestart-sourceend
56375719
[`buf.entries()`]: #bufentries
56385720
[`buf.fill()`]: #buffillvalue-offset-end-encoding

doc/api/deprecations.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4727,7 +4727,7 @@ calling or overriding `_listen2`.
47274727
[`--pending-deprecation`]: cli.md#--pending-deprecation
47284728
[`--throw-deprecation`]: cli.md#--throw-deprecation
47294729
[`--unhandled-rejections`]: cli.md#--unhandled-rejectionsmode
4730-
[`Buffer.allocUnsafeSlow(size)`]: buffer.md#static-method-bufferallocunsafeslowsize
4730+
[`Buffer.allocUnsafeSlow(size)`]: buffer.md#static-method-bufferallocunsafeslowsize-alignment
47314731
[`Buffer.from(array)`]: buffer.md#static-method-bufferfromarray
47324732
[`Buffer.from(buffer)`]: buffer.md#static-method-bufferfrombuffer
47334733
[`Buffer.isBuffer()`]: buffer.md#static-method-bufferisbufferobj
@@ -4862,7 +4862,7 @@ calling or overriding `_listen2`.
48624862
[`writable.writableLength`]: stream.md#writablewritablelength
48634863
[`zlib.bytesWritten`]: zlib.md#zlibbyteswritten
48644864
[alloc]: buffer.md#static-method-bufferallocsize-fill-encoding
4865-
[alloc_unsafe_size]: buffer.md#static-method-bufferallocunsafesize
4865+
[alloc_unsafe_size]: buffer.md#static-method-bufferallocunsafesize-alignment
48664866
[caveats of asynchronous customization hooks]: module.md#caveats-of-asynchronous-customization-hooks
48674867
[from_arraybuffer]: buffer.md#static-method-bufferfromarraybuffer-byteoffset-length
48684868
[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: 76 additions & 9 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+
const kMaxAlignment = 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+
const kPoolAlignment = 64;
184+
174185
Buffer.poolSize = 64 * 1024;
175-
let poolSize, 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+
let poolSize, poolOffset, poolBase, allocPool, allocBuffer;
176190

177191
function createPool() {
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 = function allocUnsafe(size) {
469+
Buffer.allocUnsafe = function allocUnsafe(size, alignment) {
450470
validateNumber(size, 'size', 0, kMaxLength);
451-
return allocate(size);
471+
if (alignment === undefined) {
472+
return allocate(size);
473+
}
474+
validateAlignment(size, alignment);
475+
return allocateAligned(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
489+
* @param {number} [alignment] A power of two, at most 2 ** 30
459490
* @returns {FastBuffer|undefined}
460491
*/
461-
Buffer.allocUnsafeSlow = function allocUnsafeSlow(size) {
492+
Buffer.allocUnsafeSlow = function allocUnsafeSlow(size, alignment) {
462493
validateNumber(size, 'size', 0, kMaxLength);
463-
return createUnsafeBuffer(size);
494+
if (alignment === undefined) {
495+
return createUnsafeBuffer(size);
496+
}
497+
validateAlignment(size, alignment);
498+
return createUnsafeAlignedBuffer(size, alignment);
464499
};
465500

501+
function validateAlignment(size, alignment) {
502+
validateInteger(alignment, 'alignment', 1, kMaxAlignment);
503+
if ((alignment & (alignment - 1)) !== 0) {
504+
throw new ERR_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+
throw new ERR_OUT_OF_RANGE(
510+
'size', `<= ${kMaxLength - (alignment - 1)}`, size);
511+
}
512+
}
513+
466514
function allocate(size) {
467515
if (size <= 0) {
468516
return new FastBuffer();
469517
}
470518
if (size < (Buffer.poolSize >>> 1)) {
471519
if (size > (poolSize - poolOffset))
472520
createPool();
473-
const b = new FastBuffer(allocPool, poolOffset, size);
521+
const b = new FastBuffer(allocPool, poolBase + poolOffset, size);
474522
poolOffset += size;
475523
alignPool();
476524
return b;
477525
}
478526
return createUnsafeBuffer(size);
479527
}
480528

529+
function allocateAligned(size, alignment) {
530+
if (size <= 0) {
531+
return new FastBuffer();
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+
return createUnsafeAlignedBuffer(size, alignment);
538+
}
539+
poolOffset = (poolOffset + alignment - 1) & ~(alignment - 1);
540+
if (size > (poolSize - poolOffset))
541+
createPool();
542+
const b = new FastBuffer(allocPool, poolBase + poolOffset, size);
543+
poolOffset += size;
544+
alignPool();
545+
return b;
546+
}
547+
481548
function fromStringFast(string, ops) {
482549
const maxLength = Buffer.poolSize >>> 1;
483550

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

500567
const actual = ops.write(allocBuffer, string, poolOffset, length);
501-
const b = new FastBuffer(allocPool, poolOffset, actual);
568+
const b = new FastBuffer(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-
const b = new FastBuffer(allocPool, poolOffset, length);
630+
const b = new FastBuffer(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
return new FastBuffer(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+
function createUnsafeAlignedBuffer(size, alignment) {
1115+
if (size === 0) {
1116+
return new FastBuffer();
1117+
}
1118+
1119+
const ab = createUnsafeArrayBuffer(size + alignment - 1);
1120+
return new FastBuffer(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)