对齐分配


🌐 Aligned allocations

一些操作系统接口要求它们操作的内存是对齐的,而在某些硬件上,对齐只是运行更快。最常见的前者例子是非缓冲(“直接”)文件 I/O,在 Linux 上要求缓冲区地址、文件偏移量和传输长度都必须是底层设备逻辑块大小的整数倍:

🌐 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:

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();
}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;
  });
});

即使没有接口要求,从性能角度考虑,请求对齐也是值得的。将一个频繁使用的 Buffer 对齐到缓存行大小(在大多数现代 CPU 上为 64 字节)可以避免它跨越多余的缓存行,这样一个小结构就可以通过一次缓存未命中而不是两次被取出,而页对齐(4096 字节)的分配同样有助于那些映射或固定内存的接口。这些都是微优化:在动手优化之前先进行测量,因为额外的字节并不是免费的。

🌐 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.

因为 Buffer 的内存地址不能直接选择,所以必须分配额外的字节或者跳过一些字节才能达到对齐的地址。Buffer.allocUnsafeSlow() 会多分配 alignment 个字节,如果 alignment 比这还小,就分配 8 个字节,并把返回的 Buffer 放在其中第一个合适对齐的字节上。Buffer.allocUnsafe() 则会在共享内部池中填充它的偏移量,这个池的起始位置总是 64 字节对齐的,只有当 alignment 比这个大时才会回退到自己分配内存。无论哪种情况,buf.byteOffset 通常都不是 0,buf.buffer 也比 size 大,所以访问超过 Buffer 的代码进入它底层的 ArrayBuffer 时必须考虑偏移量,就像使用池化的 Buffer 一样。

🌐 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 alignment bytes, or 8 when alignment is smaller than that, 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 Buffers.

对齐是返回的 Buffer 的一个属性,并会在其整个生命周期内保持,但它不会被其他视图继承:buf.subarray、buf.slice() 和 structuredClone() 都可能产生未对齐的 Buffer。

🌐 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 Buffers.

对齐在被捕获到启动快照中时也无法保持:内存不会在序列化过程中保留其地址,所以在 --build-snapshot 生效时分配的 Buffer 在反序列化后的进程中不会对齐。如果对齐必须在运行时保持,可以在 v8.startupSnapshot.setDeserializeMainFunction() 回调内分配,或者在启动后分配。

🌐 Alignment also does not survive being captured in a startup snapshot: memory does not keep its address across serialization, so a Buffer allocated while --build-snapshot is in effect is not aligned in the deserialized process. Allocate inside a v8.startupSnapshot.setDeserializeMainFunction() callback, or after startup, if the alignment has to hold at run time.