이 기사에서는 NodeJs의 버퍼 캐시 영역을 이해하고 Node.js의 버퍼 구현을 소개합니다.
관련 지식 포인트
먼저 JavaScript에서 ArrayBuffer의 인터페이스와 배경에 대해 이야기해 보겠습니다. 다음 내용은 ECMAScript 6 Getting Started with ArrayBuffer 에서 가져온 것입니다. .
ArrayBuffer 객체, TypedArray 뷰 및 DataView 뷰는 JavaScript가 바이너리 데이터를 조작하기 위한 인터페이스입니다. 이러한 객체는 오랫동안 별도의 사양(2011년 2월 출시)으로 존재해 왔으며 ES6에서는 이를 ECMAScript 사양에 통합하고 새로운 메서드를 추가했습니다. 이들은 모두 배열 구문을 사용하여 이진 데이터를 처리하므로 집합적으로 이진 배열이라고 합니다.
이 인터페이스의 원래 디자인 목적은 WebGL 프로젝트와 관련이 있습니다. 소위 WebGL은 브라우저와 그래픽 카드 사이의 통신 인터페이스를 의미합니다. JavaScript와 그래픽 카드 간의 대규모 실시간 데이터 교환을 충족하려면 둘 사이의 데이터 통신이 바이너리가 아닌 바이너리여야 합니다. 전통적인 텍스트 형식. 32비트 정수가 텍스트 형식으로 전달되면 양쪽 끝의 JavaScript 스크립트와 그래픽 카드가 형식 변환을 수행해야 하므로 시간이 많이 걸립니다. 이때 C언어처럼 바이트를 직접 연산하여 4바이트 32비트 정수를 바이너리 형태로 그대로 그래픽 카드에 보낼 수 있는 메커니즘이 있다면 스크립트의 성능은 크게 향상될 것이다.
이진 배열은 이러한 맥락에서 탄생했습니다. 이는 C 언어의 배열과 매우 유사하여 개발자가 배열 첨자 형태로 메모리를 직접 조작할 수 있게 하며, 이는 JavaScript의 바이너리 데이터 처리 능력을 크게 향상시켜 개발자가 C 언어의 기본 인터페이스와 바이너리 통신을 수행할 수 있게 해줍니다. JavaScript를 통한 운영 체제.
이 글을 읽고 나면 ArrayBuffer 시리즈 인터페이스를 통해 JavaScript가 바이너리 데이터를 처리할 수 있다는 것을 알 수 있습니다. 사용법은 주로 다음 단계로 나뉩니다.
ArrayBuffer 생성자를 통해 길이 10의 메모리 영역을 만듭니다
ArrayBuffer를 가리키도록 Uint8Array 생성자를 통해 매개변수 전달
배열을 작동하는 것처럼 첫 번째 바이트 123에 데이터 쓰기
const buf1 = new ArrayBuffer(10); const x1 = new Uint8Array(buf1); x1[0] = 123;
ArrayBuffer는 Node.js에서도 사용할 수 있습니다. 처리할 관련 인터페이스 ArrayBuffer 및 Buffer 문서를 주의 깊게 읽은 후 Buffer를 추가로 캡슐화하면 더 쉽게 시작하고 더 나은 성능을 얻을 수 있다는 것을 알 수 있습니다. 그런 다음 Buffer
의 사용 예를 살펴보겠습니다.alloc 메소드를 통해 길이 10의 메모리 영역을 생성합니다
writeUInt8을 통해 첫 번째 바이트에 데이터 123을 씁니다
readUint8
const buf1 = Buffer.alloc(10); buf1.writeUInt8(123, 0) buf1.readUint8(0)
정적 메서드 alloc을 통해 Buffer 인스턴스 생성
팁: Buffer 생성자를 통해 직접 인스턴스를 생성하는 방법은 보안 문제로 인해 중단되었습니다.
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가 실제로는 Uint8Array라는 것을 확인했습니다. JavaScript에서도 사용된다는 점을 추가하세요. ArrayBuffer 객체를 사용하지 않고 Uint8Array를 사용하여 직접 메모리를 조작할 수 있습니다. 예를 들어 다음 예제에서는 Uint8Array 생성자를 통해 길이가 10인 메모리 영역을 생성합니다.
const x1 = new Uint8Array(10); x1[0] = 123
提供了 alloc, allocUnsafe, allocUnsafeSlow 3个方法去创建一个 Buffer 实例, 上面讲了 alloc 方法没有什么特别, 下面讲一下另外两种方法
与 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种状态。
当我们需要一个Buffer对象,可以通过以下方式分配指定大小的Buffer对象:
new Buffer(size); Node以8 KB为界限来区分Buffer是大对象还是小对象: Buffer.poolSize = 8 * 1024; 这个8 KB的值也就是每个slab的大小值,在JavaScript层面,以它作为单位单元进行内存的分配。
比起 allocUnsafe 从预先申请好的 allocPool 内存中切割出来的内存区, allocUnsafeSlow 是直接通过 createUnsafeBuffer 先创建的内存区域。从命名可知直接使用 Uint8Array 等都是 Slow 缓慢的。
Buffer.allocUnsafeSlow = function allocUnsafeSlow(size) { assertSize(size); return createUnsafeBuffer(size); };
这个 Unsafe 不安全又是怎么回事了, 其实我们发现直接通过 Uint8Array 申请的内存都是填充了 0 数据的认为都是安全的, 那么 Node.js 又做了什么操作使其没有被填充数据了 ?
let zeroFill = getZeroFillToggle(); function createUnsafeBuffer(size) { zeroFill[0] = 0; try { return new FastBuffer(size); } finally { zeroFill[0] = 1; } }
那么我们只能去探究一下 zeroFill 在创建前后, 类似开关的操作的是如何实现这个功能
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)); }
内存分配器的实现
从代码实现可以看到如果 zero_fill_field 值为
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; }
接着 Allocate 函数的内容
关于 calloc 与 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)
对写入的数值范围为 -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, 'offset'); if (value > max || value < min) { throw new ERR_OUT_OF_RANGE('value', `>= ${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, 'offset'); const val = this[offset]; if (val === undefined) boundsError(offset, this.length - 1); return val | (val & 2 ** 7) * 0x1fffffe; }
计算机中的有符号数有三种表示方法,即原码、反码和补码。三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”,而数值位,三种表示方法各不相同。在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理。
通过 Uint8Array 如何写入读取 Uint16Array 数据?
从下面的代码也是逐渐的看清了 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, 'offset'); 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 指的是小端字节序, 使用何种方式都是可以的。小端字节序写用小端字节序读, 端字节序写就用大端字节序读, 读写规则不一致则会造成乱码, 更多可见 理解字节序。
对于 float32Array 的实现, 相当于直接使用了 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, 'offset'); 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 编码实现。
更多编程相关知识,请访问:编程视频!!
위 내용은 Nodejs의 버퍼 캐시 영역에 대한 심층적인 이해의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!