NodeJS的Buffer和ArrayBuffer的异同

文章部分内容参考并翻译自 StackOverflow – Convert a binary NodeJS Buffer to JavaScript ArrayBuffer: Answer from Константин Ван

1. Buffer 是一个用于操作 ArrayBuffer 的视图(view)

一个 Buffer 对象(实际上是 FastBuffer)继承自 Uint8Array。而 Uint8Array 则是8位无符号整型数组(一段以8bit数据为单位的无符号整型数组),是 TypedArray 的一种。

📜/lib/buffer.js#L306-L320 Node.js 9.4.0

class FastBuffer extends Uint8Array {
  constructor(arg1, arg2, arg3) {
    super(arg1, arg2, arg3);
  }
}
FastBuffer.prototype.constructor = Buffer;
internalBuffer.FastBuffer = FastBuffer;

Buffer.prototype = FastBuffer.prototype;

补充:

Buffer.from(arrayBuffer) 方法中,若传入的是一个 ArrayBuffer,那会直接生成一个 FastBuffer 对象,而前文说道,FastBuffer 是直接继承了 Uint8Array。类似的,Buffer.alloc 方法也是会返回一个 FastBuffer 对象。

Buffer.alloc = function alloc(size, fill, encoding) {
  assertSize(size);
  if (fill !== undefined && fill !== 0 && size > 0) {
    const buf = createUnsafeBuffer(size);
    return _fill(buf, fill, 0, buf.length, encoding);
  }
  return new FastBuffer(size);
};

// 创建一个未初始化的buffer
function createUnsafeBuffer(size) {
  zeroFill[0] = 0;
  try {
    return new FastBuffer(size);
  } finally {
    zeroFill[0] = 1;
  }
}

作者:双师傅
链接:https://juejin.im/post/5ce6a17df265da1b8e707881
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

2. ArrayBuffer的大小与其视图(view)的大小不一定一致

原因1: Buffer.from(arrayBuffer[, byteOffset[, length]])

Buffer.from(arrayBuffer[, byteOffset[, length]]) 方法中,你可以传入一个 ArrayBuffer及其视图的位置和长度来创建一个 Buffer 对象。

const test_buffer = Buffer.from(new ArrayBuffer(50), 40, 10);
console.info(test_buffer.buffer.byteLength); // 50; ArrayBuffer占用内存的大小
console.info(test_buffer.length); // 10; 视图的大小

我们先通过 new ArrayBuffer 在内存中请求了一段长度为50字节的内存空间,然后通过 Buffer.from 得到一个从第40字节开始、长度为10字节的视图。我们可以看到 Buffer 对象的 buffer 属性的大小仍是50字节,但是这个视图本身的大小是10字节。所以我们可以得出一个结论就是 ArrayBuffer 的大小和其视图(view)的大小不一定一致。

原因2: FastBuffer 的内存分配方式

创建一个Buffer 对象时,JavaScript会根据请求内存的大小,分成两种方式来分配内存。

  • 若其大小小于内存池(memory pool)的一半且不为0的时候,它会用整个内存池去分配所需要的内存
  • 否则将创建一个与所需大小一致的 ArrayBuffer 对象

📜/lib/buffer.js#L306-L320 Node.js 9.4.0

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

📜/lib/buffer.js#L98-L100 Node.js 9.4.0

function createUnsafeBuffer(size) {
  return new FastBuffer(createUnsafeArrayBuffer(size));
}

上文的”内存池(memory pool)”指什么?

内存池(memory pool)是一段大小固定、预分配的连续内存块

其目的是保持这些小内存块足够紧凑,以防止因存在大量单独管理的小内存块而产生的大量无法被利用的内存碎片

在 JavaScript 里,所谓的“内存池(memory pool)”就是默认大小(由 Buffer.poolSize 定义)为 8KiB(8192 Bytes) 的 ArrayBuffer

当其为 Buffer 对象提供小内存块的时候,它会先检查在上一个内存池内是否有足够的可用内存,若存在则在这个内存池的内存(ArrayBuffer)上创建一段视图(view),即 Buffer;否则将会创建一个新的内存池。

上文说过,咋们可以通过 Buffer 对象的 buffer 属性去访问底层的 ArrayBuffer

那在这里,我们可以给这句话补充一点:

一个小的Buffer对象的buffer属性是一个ArrayBuffer对象,若这个Buffer对象小于内存池大小的一半且不为0,那么其buffer属性是整个内存池。

这里我们可以通过代码验证下:

let a = Buffer.from([0x12])
let c = Buffer.from([0x03])
let d = Buffer.allocUnsafe(Buffer.poolSize >>> 1)
let e = Buffer.from([0x04])

console.log(a.buffer === c.buffer)  // true
console.log(c.buffer === d.buffer)  // false
console.log(c.buffer === e.buffer)  // true

Buffer对象 a 在创建的时候,创建了一个 8 KiB 的内存池。随后在我们创建 c 的时候,Node发现之前c的大小符合上文所说的 大小小于内存池(memory pool)的一半且不为0 的条件,所以直接在 a 的内存池上创建一个长度为1字节的视图,并把 [0x03] 放入到 a 的内存池里。所以 a.bufferc.buffer 是同一个对象。

但 对象 d 因为其长度是内存池大小的一半,不符合使用公共内存池的条件,所以会请求一段单独的连续内存空间。所以 c.bufferd.buffer 不是同一个对象。

所以当 Buffer 使用内存池的情况下, ArrayBuffer 的大小等于内存池的大小,与 Buffer 视图的大小将会不一致。

const zero_sized_buffer = Buffer.allocUnsafe(0);
const small_buffer = Buffer.from([0xC0, 0xFF, 0xEE]);
const big_buffer = Buffer.allocUnsafe(Buffer.poolSize >>> 1);
const huge_buffer = Buffer.allocUnsafe(Buffer.poolSize << 1);

// 一个 `Buffer`对象的 `length` 属性返回他(视图)的大小,单位是字节。
// 一个 `ArrayBuffer`对象的 `byteLength` 属性返回他数据的大小,单位也是字节。

console.info(zero_sized_buffer.length); // 0字节; 视图的大小
console.info(zero_sized_buffer.buffer.byteLength); // 0字节; 视图对应的 ArrayBuffer的大小
console.info(Buffer.poolSize); // 8192; 一个内存池默认的大小

console.info(small_buffer.length); // 3字节; 视图的大小
console.info(small_buffer.buffer.byteLength); // 8192字节; 视图对应的 ArrayBuffer的大小
console.info(Buffer.poolSize); // 8192; 一个内存池默认的大小


// 下面是当Buffer请求内存大于等于默认内存池大小一半的时候的情况
console.info(big_buffer.length); // 4096; the view's size.
console.info(big_buffer.buffer.byteLength); // 4096; 视图对应的 ArrayBuffer的大小
console.info(Buffer.poolSize); // 8192; 一个内存池默认的大小

console.info(huge_buffer.length); // 16384; the view's size.
console.info(huge_buffer.buffer.byteLength); // 16384; 视图对应的 ArrayBuffer的大小
console.info(Buffer.poolSize); // 8192; 一个内存池默认的大小

3.所以我们需要提取内存到视图里

一个 ArrayBuffer 的大小是固定的,所以如果我们通过拷贝这部分数据来将其提取出来。要实现这一点,我们需要通过 BufferUint8Array 继承到的 byteOffset 属性和 length 属性以及 ArrayBuffer.property.slice 方法去获取 ArrayBuffer 中的这部分数据。

const test_buffer = Buffer.from(new ArrayBuffer(10));
const zero_sized_buffer = Buffer.allocUnsafe(0);
const small_buffer = Buffer.from([0xC0, 0xFF, 0xEE]);
const big_buffer = Buffer.allocUnsafe(Buffer.poolSize >>> 1);

function extract_arraybuffer(buf)
{
    // You may use the `byteLength` property instead of the `length` one.
    return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.length);
}

// A copy -
const test_arraybuffer = extract_arraybuffer(test_buffer); // of the memory.
const zero_sized_arraybuffer = extract_arraybuffer(zero_sized_buffer); // of the... void.
const small_arraybuffer = extract_arraybuffer(small_buffer); // of the part of the memory.
const big_arraybuffer = extract_arraybuffer(big_buffer); // of the memory.

console.info(test_arraybuffer.byteLength); // 10
console.info(zero_sized_arraybuffer.byteLength); // 0
console.info(small_arraybuffer.byteLength); // 3
console.info(big_arraybuffer.byteLength); // 4096

4. 性能优化

如果你想把这部分数据用作只读用途,或者你容许 Buffer 对象中的数据被修改,那么你可以避免一些不必要的内存复制。

const test_buffer = Buffer.from(new ArrayBuffer(10));
const zero_sized_buffer = Buffer.allocUnsafe(0);
const small_buffer = Buffer.from([0xC0, 0xFF, 0xEE]);
const big_buffer = Buffer.allocUnsafe(Buffer.poolSize >>> 1);

function obtain_arraybuffer(buf)
{
    if(buf.length === buf.buffer.byteLength)
    {
        return buf.buffer;
    } // else:
    // subarray 方法继承自 Uint8Array,可以对TypedArray数组的一部分,再建立一个新的视图。(实际上指向的还是同一段内存块)
    return buf.subarray(0, buf.length);
}

// Its underlying `ArrayBuffer`.
const test_arraybuffer = obtain_arraybuffer(test_buffer);
// Just a zero-sized `ArrayBuffer`.
const zero_sized_arraybuffer = obtain_arraybuffer(zero_sized_buffer);
// A copy of the part of the memory.
const small_arraybuffer = obtain_arraybuffer(small_buffer);
// Its underlying `ArrayBuffer`.
const big_arraybuffer = obtain_arraybuffer(big_buffer);

console.info(test_arraybuffer.byteLength); // 10
console.info(zero_sized_arraybuffer.byteLength); // 0
console.info(small_arraybuffer.byteLength); // 3
console.info(big_arraybuffer.byteLength); // 4096

结论

所以通过这整个实例,我们可以得到一个结论,就是 Buffer 约等于 Uint8Array 因为 Buffer 就是通过继承 Uint8Array 实现的。

Uint8Array 是9种 TypedArray 视图中的一种,而 ArrayBuffer 是这些内存块,直接存储二进制数据。打个比方,如果说 ArrayBuffer 像是一块标本,而这些视图像是一个显微镜操作台,你可以拿到这个 ArrayBuffer 但是你需要这些 TypedArray 视图来操作这些数据。

分享到:

0 条评论

昵称

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据

与博主谈论人生经验?