buffer: support aligned allocations · nodejs/node@615273d · GitHub
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

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+
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
459-
* @returns {FastBuffer|undefined}
489+
* @param {number} [alignment] A power of two, at most 2 ** 30
490+
* @returns {FastBuffer}
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

0 commit comments

Comments
 (0)