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>
Adds an optional alignment argument to Buffer.allocUnsafe() and
Buffer.allocUnsafeSlow(), guaranteeing that the memory backing the returned
buffer starts at an address that is a multiple of alignment.
Why
Some OS 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
device's logical block size. There was previously no way to get such a buffer
from JS — the address of a backing store can neither be observed nor chosen.
Alignment is also useful purely for performance, e.g. keeping a hot buffer from
straddling one more cache line than its size requires.
How
V8 does not allow picking the address of a backing store, so alignment is
achieved by over-allocating alignment - 1 bytes and positioning the buffer at
the first suitably aligned byte within them. A new arrayBufferAlignedOffset()
binding computes that offset.
It returns an offset rather than a finished Buffer for two reasons: external
backing stores are rejected outright when the V8 sandbox is enabled, and
Buffer::New() is not usable during buffer.js evaluation — the buffer
prototype is only wired up after require('buffer') returns, which the pool
creation runs before.
The Buffer.allocUnsafe() pool is now cache-line aligned itself, so pooled
allocations can satisfy any alignment up to 64 bytes by padding their offset
into the pool instead of allocating separately. poolOffset is therefore now
tracked relative to a new poolBase.
Status
lint-md reports only the expected pr-url: REPLACEME warnings.