为 Node.js 构建二进制编译器
Source: Dev.to
请提供您希望翻译的完整文本(除代码块和 URL 之外的内容),我将按照要求将其翻译成简体中文并保留原有的格式、Markdown 语法以及技术术语。谢谢!
概述
二进制编译器接受一种数据格式并生成纯粹、平坦的二进制数据。
data → buffer
出现这些技术(protobuf、flatbuffers,等等)的原因很简单:当前的互联网通信标准臃肿。是的,JSON 的效率极低,但这是我们为可调试性和可读性付出的代价。
这里的这个数字 →
1在 JSON 中比纯二进制大 200 %。
'1' = 8 bits
'"' = 8 bits
'"' = 8 bits
----------------
Total: 24 bits
在纯二进制中它只有 8 bits。
但这还不是最离谱的地方。JSON 中的每个值还必须携带一个 键:
{
"key": "1"
}
想一想,这现在需要多少位?
别只听我说——下面有证据。
示例对象
const obj = {
age: 26,
randNum16: 256,
randNum16: 16000,
randNum32: 33000,
hello: "world",
thisisastr: "a long string lowkey",
}
大小对比
obj.json 98 kb
obj.bin 86 kb # ← 没有协议(键 + 值序列化)
obj2.bin 41 kb # ← 有协议(仅值,协议负责键)
即使键已编码,纯二进制仍然更为紧凑,且随着负载增大,节省的空间会迅速累积。
性能
Execution time comparison (milliseconds):
Name | Count | Min(ms) | Max(ms) | Mean(ms) | Median(ms) | StdDev(ms)
-----+-------+---------+---------+----------+------------+-----------
JSON | 100 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000
TEMU | 100 | 0.000 | 1.000 | 0.010 | 0.000 | 0.099
最长的一次运行用了 1 ms,这其实有点误导——在 100 次采样中它是唯一的异常值。我的猜测是:Node 在分配初始缓冲区时产生的开销。
Source:
为什么需要二进制编译器?
我最近一直在为 Node.js 构建更多实时系统和工具,而 JSON 是带宽大户。在以 JSON 作为主要传输方式的情况下,构建低延迟系统几乎是不可想象的。
这也是 protobuf 等技术出现的原因,尤其适用于服务器间通信。它们速度极快,且体积更小。
我并不是在使用通用方案,而是尝试自己动手实现,以此为我想要带到 Node.js 的项目奠定基础:
- tessera.js – 一个使用原始字节(
SharedArrayBuffer)的 C++ N‑API 渲染器
How I built a renderer for Node.js - shard – 一个亚 100 纳秒延迟的分析器(原生 C/C++ 通常在 5–40 ns 左右)
- nexus – 一个类似 Godot 的 Node.js 游戏引擎
这个实验实际上是为这些项目准备可靠的二进制编码/解码器。
构建编译器
注意: 这属于实验性质。我真的就是打开 VS Code,直接开始写代码——没有做任何调研,也没有阅读论文。
说实话,这是学习任何东西的最佳方式:先凭直觉糟糕地实现一次,然后再看看专家是怎么做的。你会注意到这里的命名几乎没有经过思考,完全是随性而为。这是有意为之;这就是我的原型开发方式。
工具和设置
import { writeFileSync, fstatSync, openSync, closeSync } from "fs";
const obj = {
age: 26,
randNum16: 256,
randNum16: 16000,
randNum32: 33000,
hello: "world",
thisisastr: "a long string lowkey",
// stack: ['c++', "js", "golang"],
// hobbies: ["competitive gaming", "hacking node.js", "football"]
};
const TYPES = {
numF: 1, // float
numI8: 2, // int8
numI16: 3, // int16
numI32: 4, // int32
string: 5,
array: 6,
};
function getObjectKeysAndValues(obj) {
// JS preserves property order per spec
const keys = Object.keys(obj);
const values = Object.values(obj);
return [keys, values];
}
function isFloat(num) {
return !Number.isInteger(num);
}
序列化键
简单协议:
[allKeysLen | keyLen | key] → buffer
function serKeys(keys) {
let len = 0;
for (let i = 0; i 255)
throw new Error(`Key too long: "${k}" (${k.length} bytes)`);
buf.writeUInt8(k.length, writer++);
const written = buf.write(k, writer, "utf8");
writer += written;
}
return buf;
}
反序列化就是逆过程:读取长度 → 读取键 → 移动指针。
function deserKeys(buf) {
let reader = 2;
const keys = [];
while (reader = -128 && num = -32768 && num = -128 && num = -32768 && num 8 位
i16 -> 16 位
i32 -> 32 位
编译器
序列化
function gen(obj, protocol = false) {
if (typeof obj !== "object")
throw new Error("Must be Object");
let cache = new Map();
const [keys] = getObjectKeysAndValues(obj);
let serk;
if (!protocol) {
serk = serKeys(keys);
}
let length = 0;
for (const key of keys) {
let buf;
switch (typeof obj[key]) {
case "number":
buf = seNumber(obj[key]);
break;
case "string":
buf = seString(obj[key]);
break;
default:
continue;
}
length += buf.length;
cache.set(key, buf);
}
const dataBuf = Buffer.allocUnsafe(length);
let writer = 0;
for (const key of keys) {
const b = cache.get(key);
if (b) {
b.copy(dataBuf, writer);
writer += b.length;
}
}
return protocol ? dataBuf : Buffer.concat([serk, dataBuf]);
}
反序列化
function unserData(buf) {
let reader = 0;
let data = [];
while (reader < buf.length) {
const t = buf.readInt8(reader++);
switch (t) {
case 1:
data.push(buf.readFloatBE(reader));
reader += 4;
break;
case 2:
data.push(buf.readInt8(reader++));
break;
case 3:
data.push(buf.readInt16BE(reader));
reader += 2;
break;
case 4:
data.push(buf.readInt32BE(reader));
reader += 4;
break;
case 5:
const len = buf.readInt16BE(reader);
reader += 2;
data.push(buf.subarray(reader, reader + len).toString("utf8"));
reader += len;
break;
}
}
return data;
}
统一解析器
function ungen(buf, protocol = false) {
if (!protocol) {
const keysLen = buf.readInt16BE(0);
const keysBuf = buf.subarray(0, keysLen);
deserKeys(keysBuf);
return unserData(buf.subarray(keysLen));
}
return unserData(buf);
}
运行它
健全性检查
let samples = { JSON: [], TEMU: [] };
function J() {
const start = process.hrtime.bigint();
JSON.parse(JSON.stringify(obj));
const end = process.hrtime.bigint();
samples.JSON.push((end - start) / 1_000_000n);
}
function T() {
const start = process.hrtime.bigint();
const b = gen(obj, true);
ungen(b);
const end = process.hrtime.bigint();
samples.TEMU.push((end - start) / 1_000_000n);
}
采样
const WARMUP = 100_000;
const SAMPLE = 100;
for (let i = 0; i < WARMUP; i++) {}
for (let i = 0; i < SAMPLE; i++) J();
for (let i = 0; i < WARMUP; i++) {}
for (let i = 0; i < SAMPLE; i++) T();
console.dir(samples.TEMU);
它可以工作。
真正的问题是: 在进行适当的研究后,这还能提升多少?