Heim >Web-Frontend >js-Tutorial >Detailliertes Verständnis des Puffer-Cache-Bereichs in Nodejs

Detailliertes Verständnis des Puffer-Cache-Bereichs in Nodejs

青灯夜游
青灯夜游nach vorne
2021-07-21 10:40:132347Durchsuche

In diesem Artikel erfahren Sie mehr über den Puffer-Cache-Bereich in NodeJs und stellen die Implementierung von Buffer in Node.js vor.

Detailliertes Verständnis des Puffer-Cache-Bereichs in Nodejs

Beteiligte Wissenspunkte

ArrayBuffer

Lassen Sie uns zunächst über die Schnittstelle und den Hintergrund von ArrayBuffer in JavaScript sprechen. Der folgende Inhalt stammt aus ECMAScript 6 Erste Schritte mit ArrayBuffer .

ArrayBuffer-Objekt, TypedArray-Ansicht und DataView-Ansicht sind eine Schnittstelle für JavaScript zum Bearbeiten von Binärdaten. Diese Objekte existieren seit langem als separate Spezifikation (veröffentlicht im Februar 2011), und ES6 hat sie in die ECMAScript-Spezifikation integriert und neue Methoden hinzugefügt. Sie alle verarbeiten Binärdaten mithilfe der Array-Syntax und werden daher zusammenfassend als Binär-Arrays bezeichnet.

Der ursprüngliche Designzweck dieser Schnittstelle hängt mit dem WebGL-Projekt zusammen. Das sogenannte WebGL bezieht sich auf die Kommunikationsschnittstelle zwischen dem Browser und der Grafikkarte. Um den groß angelegten Echtzeit-Datenaustausch zwischen JavaScript und der Grafikkarte zu ermöglichen, muss die Datenkommunikation zwischen ihnen binär sein, nicht traditionelles Textformat. Wenn eine 32-Bit-Ganzzahl im Textformat übergeben wird, müssen die JavaScript-Skripte und Grafikkarten an beiden Enden eine Formatkonvertierung durchführen, was sehr zeitaufwändig ist. Wenn es zu diesem Zeitpunkt einen Mechanismus gibt, der Bytes wie die C-Sprache direkt verarbeiten und die 4-Byte-32-Bit-Ganzzahl in binärer Form intakt an die Grafikkarte senden kann, wird die Leistung des Skripts erheblich verbessert.

Binary Array wurde in diesem Zusammenhang geboren. Es ist einem Array in der C-Sprache sehr ähnlich und ermöglicht es Entwicklern, den Speicher direkt in Form von Array-Indizes zu betreiben, was die Fähigkeit von JavaScript, Binärdaten zu verarbeiten, erheblich verbessert und es Entwicklern ermöglicht, eine binäre Kommunikation mit der nativen Schnittstelle von durchzuführen Betriebssystem über JavaScript.

Nachdem wir dies gelesen haben, wissen wir, dass die Schnittstelle der ArrayBuffer-Serie JavaScript die Verarbeitung von Binärdaten ermöglicht. Die Verwendung ist hauptsächlich in die folgenden Schritte unterteilt: Erstellen Sie einen Speicherbereich mit der Länge 10 über den ArrayBuffer-Konstruktor

Übergeben Sie Parameter über den Uint8Array-Konstruktor, um auf ArrayBuffer zu verweisen
  • Schreiben Sie Daten in das erste Byte 123, genau wie beim Betreiben eines Arrays
  • const buf1 = new ArrayBuffer(10);
    const x1 = new Uint8Array(buf1);
    x1[0]  = 123;

  • Buffer
  • Sie können ArrayBuffer auch in Node.js verwenden Binärdaten. Nachdem wir die Dokumentation von
  • ArrayBuffer
und

Buffer sorgfältig gelesen haben, können wir feststellen, dass eine weitere Kapselung von Buffer den Einstieg erleichtern und eine bessere Leistung erzielen kann. Schauen wir uns dann die Anwendungsbeispiele von Buffer an

Schalten Sie eine Speicherfläche von Länge 10 durch die Alloc -Methode

    Rite -Daten 123 zum ersten Byte durch writeUint8
  • read das erste Byte von Daten über Readuint8
  • rrree
  • buffer

  • Erstellen Sie eine Buffer-Instanz über die statische Methode alloc

Tipps: Die Methode zum Erstellen einer Instanz direkt über den Buffer-Konstruktor wurde aus Sicherheitsgründen aufgegeben

const buf1 = Buffer.alloc(10);
buf1.writeUInt8(123, 0)
buf1.readUint8(0)

Ich habe festgestellt, dass Buffer tatsächlich Uint8Array ist Fügen Sie hinzu, dass es auch in JavaScript verwendet werden kann, um den Speicher zu bedienen, ohne das ArrayBuffer-Objekt zu übergeben. Erstellen Sie beispielsweise einen Speicherbereich mit einer Länge von 10 über den Uint8Array-Konstruktor erstes Byte, genau wie das Betreiben eines Arrays.

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

class FastBuffer extends Uint8Array {
  constructor(bufferOrLength, byteOffset, length) {
    super(bufferOrLength, byteOffset, length);
  }
}

Buffer in Node.js verwendet also nur die Uint8Array-Klasse. Wie simuliert man das Verhalten aller folgenden Ansichtstypen und welche anderen Erweiterungen wurden an Buffer vorgenommen?

  • Int8Array:8 位有符号整数,长度 1 个字节。
  • Uint8Array:8 位无符号整数,长度 1 个字节。
  • Uint8ClampedArray:8 位无符号整数,长度 1 个字节,溢出处理不同。
  • Int16Array:16 位有符号整数,长度 2 个字节。
  • Uint16Array:16 位无符号整数,长度 2 个字节。
  • Int32Array:32 位有符号整数,长度 4 个字节。
  • Uint32Array:32 位无符号整数,长度 4 个字节。
  • Float32Array:32 位浮点数,长度 4 个字节。
  • Float64Array:64 位浮点数,长度 8 个字节。

allocUnsafe, allocUnsafeSlow

提供了 alloc, allocUnsafe, allocUnsafeSlow 3个方法去创建一个 Buffer 实例, 上面讲了 alloc 方法没有什么特别, 下面讲一下另外两种方法

allocUnsafe

与 alloc 不同的是, allocUnsafe 并没有直接返回 FastBuffer, 而是始终从 allocPool 中类似 slice 出来的内存区。

Buffer.allocUnsafe = function allocUnsafe(size) {
  assertSize(size);
  return allocate(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);
    poolOffset += size;
    alignPool();
    return b;
  }
  return createUnsafeBuffer(size);
}

这块内容其实我也是很早之前在读朴灵大佬的深入浅出 Node.js 就有所映像, 为什么这样做了, 原因主要如下

为了高效地使用申请来的内存,Node采用了slab分配机制。slab是一种动态内存管理机制,最早

诞生于SunOS操作系统(Solaris)中,目前在一些*nix操作系统中有广泛的应用,如FreeBSD和Linux。 简单而言,slab就是一块申请好的固定大小的内存区域。slab具有如下3种状态。

  • full:完全分配状态。
  • partial:部分分配状态。
  • empty:没有被分配状态。

当我们需要一个Buffer对象,可以通过以下方式分配指定大小的Buffer对象:

new Buffer(size); Node以8 KB为界限来区分Buffer是大对象还是小对象: Buffer.poolSize = 8 * 1024; 这个8 KB的值也就是每个slab的大小值,在JavaScript层面,以它作为单位单元进行内存的分配。

allocUnsafeSlow

比起 allocUnsafe 从预先申请好的 allocPool 内存中切割出来的内存区, allocUnsafeSlow 是直接通过 createUnsafeBuffer 先创建的内存区域。从命名可知直接使用 Uint8Array 等都是 Slow 缓慢的。

Buffer.allocUnsafeSlow = function allocUnsafeSlow(size) {
  assertSize(size);
  return createUnsafeBuffer(size);
};

createUnsafeBuffer

这个 Unsafe 不安全又是怎么回事了, 其实我们发现直接通过 Uint8Array 申请的内存都是填充了 0 数据的认为都是安全的, 那么 Node.js 又做了什么操作使其没有被填充数据了 ?

let zeroFill = getZeroFillToggle();
function createUnsafeBuffer(size) {
  zeroFill[0] = 0;
  try {
    return new FastBuffer(size);
  } finally {
    zeroFill[0] = 1;
  }
}

那么我们只能去探究一下 zeroFill 在创建前后, 类似开关的操作的是如何实现这个功能

getZeroFillToggle

zeroFill 的值来自于 getZeroFillToggle 方法返回, 其实现在 src/node_buffer.cc 文件中, 整个看下来也是比较费脑。

简要的分析一下 zeroFill 的设置主要是修改了 zero_fill_field 这个变量的值, zero_fill_field 值主要使用在 Allocate 分配器函数中。

void GetZeroFillToggle(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);
  NodeArrayBufferAllocator* allocator = env->isolate_data()->node_allocator();
  Local<ArrayBuffer> ab;
  // It can be a nullptr when running inside an isolate where we
  // do not own the ArrayBuffer allocator.
  if (allocator == nullptr) {
    // Create a dummy Uint32Array - the JS land can only toggle the C++ land
    // setting when the allocator uses our toggle. With this the toggle in JS
    // land results in no-ops.
    ab = ArrayBuffer::New(env->isolate(), sizeof(uint32_t));
  } else {
    uint32_t* zero_fill_field = allocator->zero_fill_field();
    std::unique_ptr<BackingStore> backing =
        ArrayBuffer::NewBackingStore(zero_fill_field,
                                     sizeof(*zero_fill_field),
                                     [](void*, size_t, void*) {},
                                     nullptr);
    ab = ArrayBuffer::New(env->isolate(), std::move(backing));
  }

  ab->SetPrivate(
      env->context(),
      env->untransferable_object_private_symbol(),
      True(env->isolate())).Check();

  args.GetReturnValue().Set(Uint32Array::New(ab, 0, 1));
}

Allocate

内存分配器的实现

从代码实现可以看到如果 zero_fill_field 值为

  • 真值的话会调用 UncheckedCalloc 去分配内存
  • 假值则调用 UncheckedMalloc 分配内存
void* NodeArrayBufferAllocator::Allocate(size_t size) {
  void* ret;
  if (zero_fill_field_ || per_process::cli_options->zero_fill_all_buffers)
    ret = UncheckedCalloc(size);
  else
    ret = UncheckedMalloc(size);
  if (LIKELY(ret != nullptr))
    total_mem_usage_.fetch_add(size, std::memory_order_relaxed);
  return ret;
}

UncheckedCalloc UncheckedMalloc

接着 Allocate 函数的内容

  • zero_fill_field 为真值的话会调用 UncheckedCalloc, 最后通过 calloc 去分配内存
  • zero_fill_field 为假值则调用 UncheckedMalloc, 最后通过 realloc 去分配内存

关于 calloc 与 realloc 函数

  • calloc: calloc 函数得到的内存空间是经过初始化的,其内容全为0
  • realloc: realloc 函数得到的内存空间是没有经过初始化的

至此读到这里, 我们知道了 createUnsafeBuffer 创建未被初始化内存的完整实现, 在需要创建时设置 zero_fill_field 为 0 即假值即可, 同步创建成功再把 zero_fill_field 设置为 1 即真值就好了。

inline T* UncheckedCalloc(size_t n) {
  if (n == 0) n = 1;
  MultiplyWithOverflowCheck(sizeof(T), n);
  return static_cast<T*>(calloc(n, sizeof(T)));
}

template <typename T>
inline T* UncheckedMalloc(size_t n) {
  if (n == 0) n = 1;
  return UncheckedRealloc<T>(nullptr, n);
}

template <typename T>
T* UncheckedRealloc(T* pointer, size_t n) {
  size_t full_size = MultiplyWithOverflowCheck(sizeof(T), n);

  if (full_size == 0) {
    free(pointer);
    return nullptr;
  }

  void* allocated = realloc(pointer, full_size);

  if (UNLIKELY(allocated == nullptr)) {
    // Tell V8 that memory is low and retry.
    LowMemoryNotification();
    allocated = realloc(pointer, full_size);
  }

  return static_cast<T*>(allocated);
}

其他实现

通过 Uint8Array 如何写入读取 Int8Array 数据? 如通过 writeInt8 写入一个有符号的 -123 数据。

const buf1 = Buffer.alloc(10);
buf1.writeInt8(-123, 0)

writeInt8, readInt8

  • 对写入的数值范围为 -128 到 127 进行了验证

  • 直接进行赋值操作

其实作为 Uint8Array 对应的 C 语言类型为 unsigned char, 可写入的范围为 0 到 255, 当写入一个有符号的值时如 -123, 其最高位符号位为 1, 其二进制的原码为 11111011, 最终存储在计算机中所有的数值都是用补码。所以其最终存储的补码为 10000101, 10 进制表示为 133。

  • 此时如果通过 readUInt8 去读取数据的话就会发现返回值为 133

  • 如果通过 readInt8 去读取的话, 套用代码的实现 133 | (133 & 2 ** 7) * 0x1fffffe === -123 即满足要求

function writeInt8(value, offset = 0) {
  return writeU_Int8(this, value, offset, -0x80, 0x7f);
}

function writeU_Int8(buf, value, offset, min, max) {
  value = +value;
  // `checkInt()` can not be used here because it checks two entries.
  validateNumber(offset, &#39;offset&#39;);
  if (value > max || value < min) {
    throw new ERR_OUT_OF_RANGE(&#39;value&#39;, `>= ${min} and <= ${max}`, value);
  }
  if (buf[offset] === undefined)
    boundsError(offset, buf.length - 1);

  buf[offset] = value;
  return offset + 1;
}

function readInt8(offset = 0) {
  validateNumber(offset, &#39;offset&#39;);
  const val = this[offset];
  if (val === undefined)
    boundsError(offset, this.length - 1);

  return val | (val & 2 ** 7) * 0x1fffffe;
}

计算机中的有符号数有三种表示方法,即原码、反码和补码。三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”,而数值位,三种表示方法各不相同。在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理。

通过 Uint8Array 如何写入读取 Uint16Array  数据?

writeUInt16, readUInt16

从下面的代码也是逐渐的看清了 Uint8Array 的实现, 如果写入 16 位的数组, 即会占用两个字节长度的 Uint8Array, 每个字节存储 8 位即可。

function writeU_Int16BE(buf, value, offset, min, max) {
  value = +value;
  checkInt(value, min, max, buf, offset, 1);

  buf[offset++] = (value >>> 8);
  buf[offset++] = value;
  return offset;
}

function readUInt16BE(offset = 0) {
  validateNumber(offset, &#39;offset&#39;);
  const first = this[offset];
  const last = this[offset + 1];
  if (first === undefined || last === undefined)
    boundsError(offset, this.length - 2);

  return first * 2 ** 8 + last;
}

BE 指的是大端字节序, LE 指的是小端字节序, 使用何种方式都是可以的。小端字节序写用小端字节序读, 端字节序写就用大端字节序读, 读写规则不一致则会造成乱码, 更多可见 理解字节序

  • 大端字节序:高位字节在前,低位字节在后,这是人类读写数值的方法。
  • 小端字节序:低位字节在前,高位字节在后,即以0x1122形式储存。

writeFloatForwards, readFloatForwards

对于 float32Array 的实现, 相当于直接使用了 float32Array

  • 写入一个数值时直接赋值给 float32Array 第一位, 然后从 float32Array.buffe 中取出写入的 4 个字节内容
  • 读取时给 float32Array.buffe 4个字节逐个赋值, 然后直接返回 float32Array 第一位即可
const float32Array = new Float32Array(1);
const uInt8Float32Array = new Uint8Array(float32Array.buffer);

function writeFloatForwards(val, offset = 0) {
  val = +val;
  checkBounds(this, offset, 3);

  float32Array[0] = val;
  this[offset++] = uInt8Float32Array[0];
  this[offset++] = uInt8Float32Array[1];
  this[offset++] = uInt8Float32Array[2];
  this[offset++] = uInt8Float32Array[3];
  return offset;
}

function readFloatForwards(offset = 0) {
  validateNumber(offset, &#39;offset&#39;);
  const first = this[offset];
  const last = this[offset + 3];
  if (first === undefined || last === undefined)
    boundsError(offset, this.length - 4);

  uInt8Float32Array[0] = first;
  uInt8Float32Array[1] = this[++offset];
  uInt8Float32Array[2] = this[++offset];
  uInt8Float32Array[3] = last;
  return float32Array[0];
}

小结

本文主要讲了 Node.js 中 Buffer 的实现, 相比直接使用 Uint8Array 等在性能安全以及使用上方便层度上做了一些改造, 有兴趣的同学可以扩展阅读 gRPC 中的 Protocol Buffers 的实现, 其遵循的是 Varints 编码 与 Zigzag 编码实现。

更多编程相关知识,请访问:编程视频!!

Das obige ist der detaillierte Inhalt vonDetailliertes Verständnis des Puffer-Cache-Bereichs in Nodejs. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:juejin.cn. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen