Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 86 additions & 4 deletions doc/api/buffer.md
Original file line number Diff line number Diff line change
Expand Up @@ -793,11 +793,14 @@ data that might not have been allocated for `Buffer`s.

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

### Static method: `Buffer.allocUnsafe(size)`
### Static method: `Buffer.allocUnsafe(size[, alignment])`

<!-- YAML
added: v5.10.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/65003
description: Added the `alignment` argument.
- version: v20.0.0
pr-url: https://github.com/nodejs/node/pull/45796
description: Throw ERR_INVALID_ARG_TYPE or ERR_OUT_OF_RANGE instead of
Expand All @@ -812,6 +815,9 @@ changes:
-->

* `size` {integer} The desired length of the new `Buffer`.
* `alignment` {integer} If given, the memory backing the new `Buffer` will start

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we make this an options object that takes alignment?

at an address that is a multiple of `alignment`. Must be a power of two no
larger than `2 ** 30`. See [Aligned allocations][].
* Returns: {Buffer}

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

### Static method: `Buffer.allocUnsafeSlow(size)`
### Static method: `Buffer.allocUnsafeSlow(size[, alignment])`

<!-- YAML
added: v5.12.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/65003
description: Added the `alignment` argument.
- version: v20.0.0
pr-url: https://github.com/nodejs/node/pull/45796
description: Throw ERR_INVALID_ARG_TYPE or ERR_OUT_OF_RANGE instead of
Expand All @@ -883,6 +892,9 @@ changes:
-->

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

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

### Aligned allocations

Some operating system interfaces require the memory they operate on to be
aligned, and on some hardware alignment is merely faster. The most common
example of the former is unbuffered ("direct") file I/O, which on Linux requires
the buffer address, the file offset and the transfer length to all be multiples
of the logical block size of the underlying device:

```mjs
import { open } from 'node:fs/promises';
import { constants } from 'node:fs';
import { Buffer } from 'node:buffer';

const blockSize = 4096;

// The buffer address must be block-aligned for O_DIRECT to accept it.
const buf = Buffer.allocUnsafeSlow(blockSize, blockSize);

const file = await open('/dev/sda', constants.O_RDONLY | constants.O_DIRECT);
try {
await file.read(buf, 0, blockSize, 0);
} finally {
await file.close();
}
```

```cjs
const fs = require('node:fs');
const { Buffer } = require('node:buffer');

const blockSize = 4096;

// The buffer address must be block-aligned for O_DIRECT to accept it.
const buf = Buffer.allocUnsafeSlow(blockSize, blockSize);

const flags = fs.constants.O_RDONLY | fs.constants.O_DIRECT;
fs.open('/dev/sda', flags, (err, fd) => {
if (err) throw err;
fs.read(fd, buf, 0, blockSize, 0, (err) => {
fs.close(fd, () => {});
if (err) throw err;
});
});
```

Alignment can also be worth requesting purely for performance, even when no
interface demands it. Aligning a hot `Buffer` to the cache line size (64 bytes on
most contemporary CPUs) keeps it from straddling one more cache line than it
needs to, so that a small structure is fetched with one cache miss instead of
two, and page-aligned (4096 bytes) allocations similarly help interfaces that map
or pin memory. These are micro-optimizations: measure before reaching for them,
since the extra bytes are not free.

Because the address of a `Buffer`'s memory cannot be chosen directly, extra bytes
have to be allocated or skipped to reach an aligned address.
[`Buffer.allocUnsafeSlow()`][] over-allocates up to `alignment - 1` bytes and
positions the returned `Buffer` at the first suitably aligned byte within them.
[`Buffer.allocUnsafe()`][] instead pads its offset into the shared internal pool,
whose start is always aligned to 64 bytes, and only falls back to an allocation
of its own when `alignment` is larger than that. Either way,
[`buf.byteOffset`][] is usually not 0 and [`buf.buffer`][] is larger than `size`,
so code that reaches past the `Buffer` into its underlying `ArrayBuffer` must
take the offset into account, as it must for pooled `Buffer`s.

The alignment is a property of the returned `Buffer` and is preserved for its
whole lifetime, but it is not inherited by other views: [`buf.subarray`][],
[`buf.slice()`][] and `structuredClone()` may all produce unaligned `Buffer`s.

[ASCII]: https://en.wikipedia.org/wiki/ASCII
[Aligned allocations]: #aligned-allocations
[Base64]: https://en.wikipedia.org/wiki/Base64
[ISO-8859-1]: https://en.wikipedia.org/wiki/ISO-8859-1
[RFC 4648, Section 5]: https://tools.ietf.org/html/rfc4648#section-5
[UTF-16]: https://en.wikipedia.org/wiki/UTF-16
[UTF-8]: https://en.wikipedia.org/wiki/UTF-8
[WHATWG Encoding Standard]: https://encoding.spec.whatwg.org/
[`Buffer.alloc()`]: #static-method-bufferallocsize-fill-encoding
[`Buffer.allocUnsafe()`]: #static-method-bufferallocunsafesize
[`Buffer.allocUnsafeSlow()`]: #static-method-bufferallocunsafeslowsize
[`Buffer.allocUnsafe()`]: #static-method-bufferallocunsafesize-alignment
[`Buffer.allocUnsafeSlow()`]: #static-method-bufferallocunsafeslowsize-alignment
[`Buffer.concat()`]: #static-method-bufferconcatlist-totallength
[`Buffer.copyBytesFrom()`]: #static-method-buffercopybytesfromview-offset-length
[`Buffer.from(array)`]: #static-method-bufferfromarray
Expand All @@ -5633,6 +5714,7 @@ introducing security vulnerabilities into an application.
[`TypedArray.prototype.subarray()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/subarray
[`blob.stream()`]: #blobstream
[`buf.buffer`]: #bufbuffer
[`buf.byteOffset`]: #bufbyteoffset
[`buf.compare()`]: #bufcomparetarget-targetstart-targetend-sourcestart-sourceend
[`buf.entries()`]: #bufentries
[`buf.fill()`]: #buffillvalue-offset-end-encoding
Expand Down
4 changes: 2 additions & 2 deletions doc/api/deprecations.md
Original file line number Diff line number Diff line change
Expand Up @@ -4727,7 +4727,7 @@ calling or overriding `_listen2`.
[`--pending-deprecation`]: cli.md#--pending-deprecation
[`--throw-deprecation`]: cli.md#--throw-deprecation
[`--unhandled-rejections`]: cli.md#--unhandled-rejectionsmode
[`Buffer.allocUnsafeSlow(size)`]: buffer.md#static-method-bufferallocunsafeslowsize
[`Buffer.allocUnsafeSlow(size)`]: buffer.md#static-method-bufferallocunsafeslowsize-alignment
[`Buffer.from(array)`]: buffer.md#static-method-bufferfromarray
[`Buffer.from(buffer)`]: buffer.md#static-method-bufferfrombuffer
[`Buffer.isBuffer()`]: buffer.md#static-method-bufferisbufferobj
Expand Down Expand Up @@ -4862,7 +4862,7 @@ calling or overriding `_listen2`.
[`writable.writableLength`]: stream.md#writablewritablelength
[`zlib.bytesWritten`]: zlib.md#zlibbyteswritten
[alloc]: buffer.md#static-method-bufferallocsize-fill-encoding
[alloc_unsafe_size]: buffer.md#static-method-bufferallocunsafesize
[alloc_unsafe_size]: buffer.md#static-method-bufferallocunsafesize-alignment
[caveats of asynchronous customization hooks]: module.md#caveats-of-asynchronous-customization-hooks
[from_arraybuffer]: buffer.md#static-method-bufferfromarraybuffer-byteoffset-length
[from_string_encoding]: buffer.md#static-method-bufferfromstring-encoding
Expand Down
2 changes: 1 addition & 1 deletion doc/api/worker_threads.md
Original file line number Diff line number Diff line change
Expand Up @@ -2234,7 +2234,7 @@ thread spawned will spawn another until the application crashes.
[`--max-old-space-size`]: cli.md#--max-old-space-sizesize-in-mib
[`--max-semi-space-size`]: cli.md#--max-semi-space-sizesize-in-mib
[`AsyncResource`]: async_hooks.md#class-asyncresource
[`Buffer.allocUnsafe()`]: buffer.md#static-method-bufferallocunsafesize
[`Buffer.allocUnsafe()`]: buffer.md#static-method-bufferallocunsafesize-alignment
[`ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST`]: errors.md#err_missing_message_port_in_transfer_list
[`ERR_WORKER_MESSAGING_ERRORED`]: errors.md#err_worker_messaging_errored
[`ERR_WORKER_MESSAGING_FAILED`]: errors.md#err_worker_messaging_failed
Expand Down
87 changes: 77 additions & 10 deletions lib/buffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ const {
markAsUntransferable,
addBufferPrototypeMethods,
createUnsafeBuffer,
createUnsafeAlignedBuffer,
asciiWrite,
latin1Write,
utf8Write,
Expand Down Expand Up @@ -171,13 +172,27 @@ const constants = ObjectDefineProperties({}, {
},
});

// The largest alignment accepted by `Buffer.allocUnsafeSlow()`. Any plausible
// I/O alignment requirement (logical block size, memory page size, huge page
// size) is well below this.
const kMaxAlignment = 2 ** 30;

// Slices handed out of the pool are 8 byte aligned relative to the start of the
// pool, so aligning the pool itself to a cache line keeps them from straddling
// one more cache line than their size requires.
const kPoolAlignment = 64;

Buffer.poolSize = 64 * 1024;
let poolSize, poolOffset, allocPool, allocBuffer;
// `poolOffset` is relative to `poolBase`, which is where the pool starts inside
// `allocPool`. The pool is over-allocated to be able to align it, so `poolBase`
// is not necessarily 0.
let poolSize, poolOffset, poolBase, allocPool, allocBuffer;

function createPool() {
poolSize = Buffer.poolSize;
allocBuffer = createUnsafeBuffer(poolSize);
allocBuffer = createUnsafeAlignedBuffer(poolSize, kPoolAlignment);
allocPool = allocBuffer.buffer;
poolBase = TypedArrayPrototypeGetByteOffset(allocBuffer);
markAsUntransferable(allocPool);
poolOffset = 0;
}
Expand Down Expand Up @@ -444,40 +459,92 @@ Buffer.alloc = function alloc(size, fill, encoding) {
/**
* Equivalent to Buffer(num), by default creates a non-zero-filled Buffer
* instance. If `--zero-fill-buffers` is set, will zero-fill the buffer.
*
* If `alignment` is given, the memory backing the returned buffer starts at an
* address that is a multiple of `alignment`. See `Buffer.allocUnsafeSlow()`.
* @param {number} size
* @param {number} [alignment] A power of two, at most 2 ** 30
* @returns {FastBuffer}
*/
Buffer.allocUnsafe = function allocUnsafe(size) {
Buffer.allocUnsafe = function allocUnsafe(size, alignment) {
validateNumber(size, 'size', 0, kMaxLength);
return allocate(size);
if (alignment === undefined) {
return allocate(size);
}
validateAlignment(size, alignment);
return allocateAligned(size, alignment);
};

/**
* By default creates a non-zero-filled Buffer instance that is not allocated
* off the pre-initialized pool. If `--zero-fill-buffers` is set, will zero-fill
* the buffer.
*
* If `alignment` is given, the memory backing the returned buffer starts at an
* address that is a multiple of `alignment`, which is required by e.g. reads
* and writes on file descriptors opened with `O_DIRECT`. Note that up to
* `alignment - 1` extra bytes are allocated to satisfy the request, and that
* the returned buffer's `byteOffset` is therefore usually non-zero.
* @param {number} size
* @returns {FastBuffer|undefined}
* @param {number} [alignment] A power of two, at most 2 ** 30
* @returns {FastBuffer}
*/
Buffer.allocUnsafeSlow = function allocUnsafeSlow(size) {
Buffer.allocUnsafeSlow = function allocUnsafeSlow(size, alignment) {
Comment thread
ronag marked this conversation as resolved.
validateNumber(size, 'size', 0, kMaxLength);
return createUnsafeBuffer(size);
if (alignment === undefined) {
return createUnsafeBuffer(size);
}
validateAlignment(size, alignment);
return createUnsafeAlignedBuffer(size, alignment);
};

function validateAlignment(size, alignment) {
validateInteger(alignment, 'alignment', 1, kMaxAlignment);
if ((alignment & (alignment - 1)) !== 0) {
throw new ERR_INVALID_ARG_VALUE(
'alignment', alignment, 'must be a power of two');
}
// Satisfying the alignment costs up to `alignment - 1` extra bytes.
if (size > kMaxLength - (alignment - 1)) {
throw new ERR_OUT_OF_RANGE(
'size', `<= ${kMaxLength - (alignment - 1)}`, size);
}
}

function allocate(size) {
if (size <= 0) {
return new FastBuffer();
}
if (size < (Buffer.poolSize >>> 1)) {
if (size > (poolSize - poolOffset))
createPool();
const b = new FastBuffer(allocPool, poolOffset, size);
const b = new FastBuffer(allocPool, poolBase + poolOffset, size);
poolOffset += size;
alignPool();
return b;
}
return createUnsafeBuffer(size);
}

function allocateAligned(size, alignment) {
if (size <= 0) {
return new FastBuffer();
}
// The pool starts at a `kPoolAlignment` aligned address, so any alignment up
// to that can be satisfied by padding the offset into the pool. Stricter
// alignments need an allocation of their own.
if (alignment > kPoolAlignment || size >= (Buffer.poolSize >>> 1)) {
return createUnsafeAlignedBuffer(size, alignment);
}
poolOffset = (poolOffset + alignment - 1) & ~(alignment - 1);
if (size > (poolSize - poolOffset))
createPool();
const b = new FastBuffer(allocPool, poolBase + poolOffset, size);
poolOffset += size;
alignPool();
return b;
}

function fromStringFast(string, ops) {
const maxLength = Buffer.poolSize >>> 1;

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

const actual = ops.write(allocBuffer, string, poolOffset, length);
const b = new FastBuffer(allocPool, poolOffset, actual);
const b = new FastBuffer(allocPool, poolBase + poolOffset, actual);

poolOffset += actual;
alignPool();
Expand Down Expand Up @@ -560,7 +627,7 @@ function fromArrayLike(obj) {
if (length < (Buffer.poolSize >>> 1)) {
if (length > (poolSize - poolOffset))
createPool();
const b = new FastBuffer(allocPool, poolOffset, length);
const b = new FastBuffer(allocPool, poolBase + poolOffset, length);
TypedArrayPrototypeSet(b, obj, 0);
poolOffset += length;
alignPool();
Expand Down
17 changes: 17 additions & 0 deletions lib/internal/buffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const {
hexWrite,
ucs2Write,
utf8WriteStatic,
arrayBufferAlignedOffset,
createUnsafeArrayBuffer,
setDetachKey,
} = internalBinding('buffer');
Expand Down Expand Up @@ -1104,12 +1105,28 @@ function createUnsafeBuffer(size) {
return new FastBuffer(createUnsafeArrayBuffer(size));
}

// Returns an uninitialized buffer of `size` bytes whose first byte is located at
// a memory address that is a multiple of `alignment`. `alignment` must be a
// power of two, and `size + alignment - 1` must not exceed the maximum buffer
// length. Since the address of a backing store cannot be chosen, `alignment - 1`
// extra bytes are allocated and skipped, which leaves the returned buffer with a
// non-zero `byteOffset` into a larger ArrayBuffer.
function createUnsafeAlignedBuffer(size, alignment) {
if (size === 0) {
return new FastBuffer();
}

const ab = createUnsafeArrayBuffer(size + alignment - 1);
return new FastBuffer(ab, arrayBufferAlignedOffset(ab, alignment), size);
}

module.exports = {
FastBuffer,
addBufferPrototypeMethods,
markAsUntransferable,
isMarkedAsUntransferable,
createUnsafeBuffer,
createUnsafeAlignedBuffer,
readUInt16BE,
readUInt32BE,
asciiWrite,
Expand Down
Loading
Loading