ソースでノードを解析します.jsにおけるBufferの8 KBプール割当てルールと固定ビット数の読み書き

5663 ワード

Node.jsでは、Bufferは、ファイルやネットワークI/Oで取得されたデータなどの潜在的な大体積データを格納するためによく使用され、符号化を指定しない場合、Bufferの形式で提供され、その地位は一般的ではないことがわかります.Bufferの作成は、内部の8 KBプールを通過する可能性があると聞いたことがあるかもしれませんが、具体的なルールは何ですか?新しいBufferインスタンスを作成できるAPIはそんなに多く、いったいどのAPIが通過するのか、どのAPIが通過しないのか.ドキュメントを読むとき、Buffer#writeUInt32BE,Buffer#readUInt32BEなどの固定ビットの数字の読み書き操作を見たことがあるかもしれませんが、具体的にはどのように実現されていますか?
一緒に行こうjsプロジェクトのlib/buffer.jsのコードを探してみましょう.

8 KBプール割当ルール


統計すると、現在のバージョンのNode.js(v 6.0)で新しいBufferクラスインスタンスを作成できるAPIは、次のとおりです.
  • new Buffer()(使用は推奨されていませんが、メモリ内の潜在的な機密情報が漏洩する可能性があります.具体的な例はここを参照してください)
  • Buffer.alloc()
  • Buffer.allocUnsafe()(メモリ内の機密情報を漏らす可能性もあるが、意味的には非常に明確)
  • Buffer.from()
  • Buffer.concat()

  • コードトレーサビリティに従って、これらのAPIは最後に2つの内部関数のうちの1つに入り、Bufferインスタンスを作成します.この2つの内部関数はそれぞれcreateBuffer()allocate()です.
    // lib/buffer.js
    // ...
    
    Buffer.poolSize = 8 * 1024;
    var poolSize, poolOffset, allocPool;
    
    function createPool() {
      poolSize = Buffer.poolSize;
      allocPool = createBuffer(poolSize, true);
      poolOffset = 0;
    }
    createPool();
    
    function createBuffer(size, noZeroFill) {
      flags[kNoZeroFill] = noZeroFill ? 1 : 0;
      try {
        const ui8 = new Uint8Array(size);
        Object.setPrototypeOf(ui8, Buffer.prototype);
        return ui8;
      } finally {
        flags[kNoZeroFill] = 0;
      }
    }
    
    function allocate(size) {
      if (size === 0) {
        return createBuffer(size);
      }
      if (size < (Buffer.poolSize >>> 1)) {
        if (size > (poolSize - poolOffset))
          createPool();
        var b = allocPool.slice(poolOffset, poolOffset + size);
        poolOffset += size;
        alignPool();
        return b;
      } else {
        return createBuffer(size, true);
      }
    }

    コードから明らかなように、最終的に作成された場合、createBuffer()関数を実行すると、8 KBプールを経由せず、allocate()関数を実行すると、入力されたデータサイズがBuffer.poolSize記号よりも小さく1ビット右にシフトした結果(この値を2で割って下に整列することに相当し、この例では4 KB)、8 KBプールにのみ使用されます(現在のプールの空き容量が不足している場合は、新しいプールを作成し、現在のプールを新しいプールに指定します).
    では、これらのAPIがどのような方法を行っているのか見てみましょう.
    // lib/buffer.js
    // ...
    
    Buffer.alloc = function(size, fill, encoding) {
      // ...
      return createBuffer(size);
    };
    
    Buffer.allocUnsafe = function(size) {
      assertSize(size);
      return allocate(size);
    };
    
    Buffer.from = function(value, encodingOrOffset, length) {
      // ...
      if (value instanceof ArrayBuffer)
        return fromArrayBuffer(value, encodingOrOffset, length);
    
      if (typeof value === 'string')
        return fromString(value, encodingOrOffset);
    
      return fromObject(value);
    };
    
    function fromArrayBuffer(obj, byteOffset, length) {
      byteOffset >>>= 0;
    
      if (typeof length === 'undefined')
        return binding.createFromArrayBuffer(obj, byteOffset);
    
      length >>>= 0;
      return binding.createFromArrayBuffer(obj, byteOffset, length);
    }
    
    function fromString(string, encoding) {
      // ...
      if (length >= (Buffer.poolSize >>> 1))
        return binding.createFromString(string, encoding);
    
      if (length > (poolSize - poolOffset))
        createPool();
      var actual = allocPool.write(string, poolOffset, encoding);
      var b = allocPool.slice(poolOffset, poolOffset + actual);
      poolOffset += actual;
      alignPool();
      return b;
    }
    
    Buffer.concat = function(list, length) {
      // ...
      var buffer = Buffer.allocUnsafe(length);
      // ...
      return buffer;
    };

    一目瞭然ですが、以下の状況が同時に成立すると、作成された新しいBufferクラスのインスタンスが内部8 KBプールを通過することになります.
  • は、Buffer.allocUnsafe,Buffer.concat,Buffer.from(パラメータはArrayBufferインスタンスではありません)およびnew Buffer(パラメータはArrayBufferインスタンスではありません)によって作成されます.
  • 受信データサイズは0ではありません.
  • 入力データのサイズは4 KB未満でなければなりません.

  • 固定ビット数読み書きAPI


    Bufferのドキュメントを読むときに、Buffer#writeUInt32BE,Buffer#readUInt32BEのようなAPIを見ると、ES 6仕様のDateViewクラスが提供する方法が考えられるかもしれません.似たようなことをしていますjsプロジェクトでは、これらのAPIの下位層を元のDateViewインスタンスに置き換えて動作するPRもあるが、このPRは現在stalledとマークされている.具体的な理由は、以下の通りである.
  • 顕著なパフォーマンス向上はありません.
  • インスタンスが初期化されると、新しいプロパティが追加されます.
  • noAssertパラメータは無効になります.

  • このPRはともかく、これらの読み書き操作は、数字の精度が32ビット以下であれば、対応方法はすべてJavaScriptによって実現され、非常に優雅であり、TypeArrayの下のクラス(Bufferで使用されているのはUint8Array)のインスタンスの要素を利用して、ビットオーバーフロー時にオーバーフロービットを捨てる仕組みを利用している.writeUInt32LEwriteUInt32BE(LEとBE、すなわち小端バイト順と大端バイト順、この記事を参照)を例にとると、32ビットの符号なし整数には4バイトの記憶が必要であり、大端バイト順の場合、最初の要素は、入力された32ビットの整数を直接24ビット右にシフトし、元の最も左の8ビットに取得し、現在の左のすべてのビットを捨てる.このように、2番目の要素は符号なしで右に16ビット、3番目の要素は8ビット、4番目の要素は移動する必要はありません(小端バイト順は逆です):
    Buffer.prototype.writeUInt32BE = function(value, offset, noAssert) {
      value = +value;
      offset = offset >>> 0;
      if (!noAssert)
        checkInt(this, value, offset, 4, 0xffffffff, 0);
      this[offset] = (value >>> 24);
      this[offset + 1] = (value >>> 16);
      this[offset + 2] = (value >>> 8);
      this[offset + 3] = value;
      return offset + 4;
    };

    これに対応して、符号なし左シフトを使用して空席を空けて|操作をマージします.
    Buffer.prototype.readUInt32BE = function(offset, noAssert) {
      offset = offset >>> 0;
      if (!noAssert)
        checkOffset(offset, 4, this.length);
    
      return (this[offset] * 0x1000000) +
          ((this[offset + 1] << 16) |
          (this[offset + 2] << 8) |
          this[offset + 3]);
    };

    このうちの(this[offset] * 0x1000000) +this[offset] << 24 |に相当する.

    最後に


    参照先:
  • https://github.com/nodejs/node/blob/master/lib/buffer.js