Maison  >  Article  >  interface Web  >  Compréhension approfondie de la zone de cache tampon dans Nodejs

Compréhension approfondie de la zone de cache tampon dans Nodejs

青灯夜游
青灯夜游avant
2021-07-21 10:40:132255parcourir

Cet article vous amènera à comprendre la zone de cache tampon dans NodeJs et à présenter l'implémentation de Buffer dans Node.js.

Compréhension approfondie de la zone de cache tampon dans Nodejs

Points de connaissances impliqués

ArrayBuffer

Parlons d'abord de l'interface et de l'arrière-plan d'ArrayBuffer en JavaScript. Le contenu suivant provient de ECMAScript 6 Getting Started with ArrayBuffer. .

L'objet ArrayBuffer, la vue TypedArray et la vue DataView sont une interface permettant à JavaScript d'exploiter les données binaires. Ces objets existent depuis longtemps en tant que spécification distincte (publiée en février 2011), et ES6 les a incorporés dans la spécification ECMAScript et a ajouté de nouvelles méthodes. Ils traitent tous les données binaires en utilisant la syntaxe des tableaux, ils sont donc collectivement appelés tableaux binaires.

Le but de conception original de cette interface est lié au projet WebGL. Ce qu'on appelle WebGL fait référence à l'interface de communication entre le navigateur et la carte graphique. Afin de permettre l'échange de données en temps réel à grande échelle entre JavaScript et la carte graphique, la communication de données entre eux doit être binaire et non binaire. format de texte traditionnel. Si un entier de 32 bits est transmis au format texte, les scripts JavaScript et les cartes graphiques aux deux extrémités doivent effectuer une conversion de format, ce qui prendra beaucoup de temps. À l'heure actuelle, s'il existe un mécanisme capable de faire fonctionner directement des octets comme le langage C et d'envoyer l'entier 4 octets 32 bits à la carte graphique intact sous forme binaire, les performances du script seront grandement améliorées.

Le tableau binaire est né dans ce contexte. Il est très similaire à un tableau en langage C, permettant aux développeurs d'exploiter directement la mémoire sous forme d'indices de tableau, ce qui améliore considérablement la capacité de JavaScript à traiter les données binaires, permettant aux développeurs d'effectuer une communication binaire avec l'interface native du système d'exploitation via JavaScript.

Après avoir lu ceci, nous savons que l'interface de la série ArrayBuffer permet à JavaScript de traiter des données binaires. Son utilisation est principalement divisée en les étapes suivantes

  • Créez une zone mémoire de longueur 10 via le constructeur ArrayBuffer

  • .

    Passez les paramètres via le constructeur Uint8Array pour pointer vers ArrayBuffer

  • Écrivez les données sur le premier octet 123, tout comme utiliser un tableau

const buf1 = new ArrayBuffer(10);
const x1 = new Uint8Array(buf1);
x1[0]  = 123;

Buffer

Vous pouvez également utiliser ArrayBuffer dans Node.js Interface associée pour traiter données binaires. Après avoir lu attentivement la documentation de ArrayBuffer et Buffer, nous pouvons constater qu'une encapsulation plus poussée de Buffer peut faciliter le démarrage et obtenir de meilleures performances. Examinons ensuite les exemples d'utilisation de Buffer

.
  • Créez une zone mémoire de longueur 10 via la méthode d'allocation

  • Écrivez les données 123 dans le premier octet via writeUInt8

  • Lisez le premier octet de données via readUint8

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

Buffer alloc.

Créez une instance Buffer via la méthode statique alloc

Conseils : La méthode de création d'une instance directement via le constructeur Buffer a été abandonnée en raison de problèmes de sécurité

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

J'ai trouvé que Buffer est en fait Uint8Array, ici je vais ajoutez qu'il est également utilisé en JavaScript. Vous pouvez utiliser directement Uint8Array pour exploiter la mémoire sans utiliser l'objet ArrayBuffer. Par exemple, dans l'exemple suivant, créez une zone mémoire d'une longueur de 10 via le constructeur Uint8Array. octet, tout comme utiliser un tableau.

    const x1 = new Uint8Array(10);
    x1[0] = 123
  • Donc Buffer dans Node.js utilise uniquement la classe Uint8Array, comment simuler le comportement de tous les types de vues suivants et quelles autres extensions ont été apportées à Buffer ?

    • 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 编码实现。

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

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer