Skip to content

Commit 3c5bd2e

Browse files
mvogttechclaude
andcommitted
docs: update README to reflect current architecture
- Replace SSE2-only description with tiered AVX-512/AVX2/SSE2 dispatch - Add performance table with benchmarks vs bufferutil - Document all operations: mask, unmask, SHA-1, base64, CRC-32C, UTF-8 validation, findHeader, batch API - Update architecture diagram: V8 C++ API bridge (not N-API), assembly layer with all .asm files and their dispatch tiers - Document CPU feature detection (cpu_tier, cpu_features, nt_threshold) - Update file structure to include all current source files - Update comparison table vs bufferutil - Add usage examples for new operations Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 712735e commit 3c5bd2e

1 file changed

Lines changed: 142 additions & 69 deletions

File tree

README.md

Lines changed: 142 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -4,48 +4,103 @@
44
[![npm version](https://img.shields.io/npm/v/asm-bufferutil.svg)](https://www.npmjs.com/package/asm-bufferutil)
55
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
66

7-
**WebSocket frame masking in hand-written x86-64 assembly with SSE2 SIMD.**
7+
**WebSocket acceleration in hand-written x86-64 assembly with tiered SIMD dispatch (AVX-512 / AVX2 / SSE2).**
88

9-
A drop-in replacement for [`bufferutil`](https://github.com/websockets/bufferutil) — the native addon that makes the [`ws`](https://github.com/websockets/ws) WebSocket library fast. Instead of C, the hot path is written in NASM assembly with SSE2 vectorized XOR operations.
9+
A drop-in replacement for [`bufferutil`](https://github.com/websockets/bufferutil) — the native addon that makes the [`ws`](https://github.com/websockets/ws) WebSocket library fast. Instead of C, the hot paths are written in NASM assembly with multi-tier SIMD vectorization, non-temporal memory paths for large payloads, and opmask branchless tails on AVX-512.
1010

1111
## Why?
1212

13-
The WebSocket protocol (RFC 6455 §5.3) requires that every client-to-server frame be masked with a 4-byte key via XOR. This is a tight loop that runs on every single message. The `ws` library delegates this to `bufferutil`, which implements it in C. This project replaces that C with hand-tuned assembly.
13+
The WebSocket protocol (RFC 6455 §5.3) requires that every client-to-server frame be masked with a 4-byte key via XOR. This is a tight loop that runs on every single message. The `ws` library delegates this to `bufferutil`, which implements it in C. This project replaces that C with hand-tuned assembly that adapts to your CPU's capabilities at runtime.
14+
15+
## Performance
16+
17+
Benchmarked on AMD Ryzen 9 7950X3D (Zen 4, AVX-512) vs `bufferutil` (C):
18+
19+
| Operation | Speedup vs bufferutil | Peak throughput |
20+
|-----------|----------------------|-----------------|
21+
| mask | 1.1–1.5× | 55 GB/s |
22+
| unmask | 1.1–1.7× | 90 GB/s |
23+
| batch unmask (64B frames) | 6–10× ||
24+
25+
Additional operations not in bufferutil:
26+
27+
| Operation | vs Node.js built-in | Notes |
28+
|-----------|-------------------|-------|
29+
| SHA-1 (SHA-NI) | 2.3× vs `crypto.createHash` | Hardware SHA-NI, ≤119 bytes |
30+
| Base64 encode || VBMI2 VPMULTISHIFTQB + VPERMB pipeline |
31+
| Header search || AVX-512 VPCMPEQB first+last byte filter |
32+
| CRC-32C || PCLMULQDQ 4-way parallel folding |
33+
| UTF-8 validation || SIMD ASCII fast path + scalar state machine |
1434

1535
## Architecture
1636

1737
```
18-
┌─────────────────────────────────────────────────────┐
19-
│ Node.js (your app, or ws internals) │
20-
│ │
21-
│ const util = require('asm-bufferutil'); │
22-
│ util.mask(source, mask, output, offset, length); │
23-
│ util.unmask(buffer, mask); │
24-
└───────────────────┬─────────────────────────────────┘
25-
│ N-API (ABI-stable across Node versions)
26-
┌───────────────────▼─────────────────────────────────┐
27-
│ ws_napi.c — C glue layer │
28-
│ │
29-
│ • Extracts raw pointers from V8 Buffer objects │
30-
│ • Zero-copy: operates directly on V8 heap memory │
31-
│ • Passes pointers + lengths to assembly via │
32-
│ System V AMD64 calling convention │
33-
└───────────────────┬─────────────────────────────────┘
34-
│ Function call (no FFI overhead)
35-
┌───────────────────▼─────────────────────────────────┐
36-
│ ws_mask_asm.asm — x86-64 NASM │
37-
│ │
38-
│ SSE2 fast path (16 bytes/cycle): │
39-
│ 1. Load 4-byte mask into XMM register │
40-
│ 2. PSHUFD to broadcast across 128 bits │
41-
│ 3. MOVDQU + PXOR + MOVDQU in a loop │
42-
│ │
43-
│ Scalar fallback (1 byte/cycle): │
44-
│ XOR byte-by-byte for the 0-15 byte remainder │
45-
└─────────────────────────────────────────────────────┘
38+
┌──────────────────────────────────────────────────────────────┐
39+
│ Node.js (your app, or ws internals) │
40+
│ │
41+
│ const util = require('asm-bufferutil'); │
42+
│ util.mask(source, mask, output, offset, length); │
43+
│ util.unmask(buffer, mask); │
44+
│ util.sha1(data); // SHA-NI hardware acceleration │
45+
│ util.base64Encode(data); // SIMD base64 │
46+
│ util.findHeader(buf, needle); │
47+
│ util.utf8Validate(buf); // SIMD UTF-8 validation │
48+
│ util.batchUnmask(data, offsets, lengths, masks, count); │
49+
└────────────────────┬─────────────────────────────────────────┘
50+
│ Direct V8 C++ API (zero N-API dispatch overhead)
51+
┌────────────────────▼─────────────────────────────────────────┐
52+
│ ws_fast_api.cc — V8 C++ API bridge │
53+
│ │
54+
��� • node::Buffer::Data() for zero-copy pointer extraction │
55+
│ ��� Batch operations: C++ loop with zero V8 API calls inside │
56+
│ • Multi-buffer parallel: ws_unmask4/ws_mask4 for 4 frames │
57+
└─────────────���──────┬─────────────────────────────────────────┘
58+
│ System V AMD64 calling convention
59+
┌────────────────────▼─────────────────────────────────────────┐
60+
│ Assembly layer (NASM, Linux x86-64) │
61+
│ │
62+
│ ws_cpu.asm — CPUID feature detection + cache sizing │
63+
│ cpu_tier: 0=scalar, 1=SSE2, 2=AVX2, │
64+
│ 3=AVX-512F+BW │
65+
│ cpu_features bitmask: GFNI, PCLMULQDQ, │
66+
│ BMI2, LZCNT, VBMI, AMD, VBMI2 │
67+
│ nt_threshold: 50% of L3 cache │
68+
│ │
69+
│ ws_mask_asm.asm — Mask/unmask with tiered dispatch: │
70+
│ • GPR 4× unroll (< 128B) │
71+
│ • AVX-512: 512B/iter, opmask tail, │
72+
│ alignment preamble, dual-stream unmask │
73+
│ • AVX2: 128B/iter, aligned stores │
74+
│ • SSE2: 64B/iter fallback │
75+
│ • NT-store paths (> L3/2 threshold) │
76+
│ • ws_unmask4/ws_mask4: 4-frame parallel │
77+
│ • ws_find_header: VPCMPEQB wide search │
78+
│ • ws_mask_gfni: GFNI experiment baseline │
79+
│ │
80+
│ ws_base64_asm.asm — Base64 encode with dispatch: │
81+
│ • AVX-512 VBMI2: VPMULTISHIFTQB+VPERMB │
82+
│ • AVX-512 VBMI: VPERMB LUT │
83+
│ • AVX2: Klomp/Muła VPSHUFB method │
84+
│ • SSE2 / scalar fallback │
85+
│ │
86+
│ ws_crc32_asm.asm — CRC-32C: │
87+
│ • PCLMULQDQ 4-way folding (≥ 256B) │
88+
│ • SSE4.2 CRC32 instruction (< 256B) │
89+
│ │
90+
│ ws_utf8_asm.asm — UTF-8 validation: │
91+
│ • AVX-512: 64B ASCII fast check │
92+
│ • AVX2: 32B ASCII fast check │
93+
│ • Scalar state machine (non-ASCII) │
94+
│ │
95+
│ ws_sha1_ni.c — SHA-1 via Intel SHA-NI intrinsics │
96+
│ (≤ 119 bytes, WebSocket handshake use) │
97+
│ │
98+
│ ws_fallback.c — C/SIMD fallback (Windows/macOS) │
99+
│ AVX2 → SSE2 → scalar dispatch │
100+
└──────────────────────────────────────────────────────────────┘
46101
```
47102

48-
## How the masking actually works
103+
## How the masking works
49104

50105
WebSocket masking is deceptively simple. Given a payload and a 4-byte mask key:
51106

@@ -55,17 +110,15 @@ masked[i] = payload[i] XOR mask[i % 4]
55110

56111
In pure JavaScript, this is a byte-at-a-time loop. In our assembly:
57112

58-
1. **Broadcast**: Take the 4-byte mask `[A, B, C, D]` and replicate it 4× into a 128-bit SSE register: `[A,B,C,D,A,B,C,D,A,B,C,D,A,B,C,D]`
59-
2. **SIMD XOR**: Load 16 payload bytes, `PXOR` with the mask register, store result. One instruction masks 16 bytes.
60-
3. **Cleanup**: Handle the 0-15 leftover bytes with scalar XOR.
61-
62-
Since the mask repeats every 4 bytes and 16 is a multiple of 4, the broadcast trick works perfectly — no alignment bookkeeping needed.
113+
1. **Broadcast**: `VPBROADCASTD` replicates the 4-byte mask across a 512-bit ZMM register (64 bytes of mask).
114+
2. **Alignment preamble**: An opmask partial store aligns the destination to a 64-byte cache line boundary, eliminating split-line penalties.
115+
3. **Bulk XOR**: 8× unrolled `VPXORD` processes 512 bytes per iteration with interleaved load-XOR-store scheduling.
116+
4. **Opmask tail**: `BZHI` builds a bitmask for the remaining 0–63 bytes; `vmovdqu8{k1}` handles the tail with zero branches.
117+
5. **NT path**: For payloads exceeding 50% of L3 cache, switches to non-temporal stores (`VMOVNTDQ`) to avoid polluting the cache hierarchy.
63118

64119
## Build
65120

66-
Prerequisites: `nasm`, `node-gyp`, Node.js ≥ 16, **Linux x86-64** for native SIMD paths.
67-
68-
On Windows and macOS, `npm install` will compile the C layer only. The assembly SIMD paths are Linux x86-64 exclusive. The pure JavaScript fallback is used automatically on other platforms.
121+
Prerequisites: **Linux x86-64** with `nasm` for assembly SIMD paths. On Windows and macOS, the C fallback (`ws_fallback.c`) provides AVX2/SSE2/scalar dispatch automatically.
69122

70123
```bash
71124
# Install NASM (Ubuntu/Debian)
@@ -75,13 +128,15 @@ sudo apt install nasm
75128
npm install
76129
npm run build
77130

78-
# Run tests
131+
# Run tests (89 tests)
79132
npm test
80133

81134
# Run benchmarks
82135
npm run bench
83136
```
84137

138+
Requires Node.js ≥ 20 and `node-gyp`.
139+
85140
## Usage
86141

87142
### Standalone
@@ -100,6 +155,18 @@ bufferUtil.mask(payload, mask, output, 0, payload.length);
100155
// Unmask (in-place)
101156
bufferUtil.unmask(output, mask);
102157
// output now equals payload
158+
159+
// SHA-1 (hardware-accelerated on CPUs with SHA-NI)
160+
const hash = bufferUtil.sha1(Buffer.from("input data"));
161+
162+
// Base64 encode
163+
const b64 = bufferUtil.base64Encode(hash);
164+
165+
// UTF-8 validation
166+
const valid = bufferUtil.utf8Validate(Buffer.from("こんにちは"));
167+
168+
// Batch unmask (packed buffer API — zero V8 overhead in inner loop)
169+
bufferUtil.batchUnmask(data, offsets, lengths, masks, count);
103170
```
104171

105172
### As a drop-in for ws
@@ -131,50 +198,56 @@ const WebSocket = require('ws');
131198

132199
## How this compares to bufferutil
133200

134-
| Aspect | bufferutil | asm-bufferutil |
135-
| ---------------- | -------------------------------- | ------------------------- |
136-
| Language | C | x86-64 Assembly |
137-
| SIMD | Compiler decides (often uses it) | Explicit SSE2, guaranteed |
138-
| Masking strategy | 32-bit XOR in C loop | 128-bit PXOR (4× wider) |
139-
| N-API version | Same | Same |
140-
| API | `mask()`, `unmask()` | `mask()`, `unmask()` |
141-
| Portability | Any platform with C compiler | Linux x86-64 only |
201+
| Aspect | bufferutil | asm-bufferutil |
202+
| ---------------- | -------------------------------- | --------------------------------------- |
203+
| Language | C | x86-64 Assembly (NASM) |
204+
| SIMD | Compiler decides | Explicit AVX-512 / AVX2 / SSE2 tiering |
205+
| Masking strategy | 32-bit XOR in C loop | 512-bit VPXORD (16× wider) |
206+
| V8 bridge | N-API | Direct V8 C++ API (lower overhead) |
207+
| Batch API | None | Packed buffer, zero V8 calls in loop |
208+
| Extra operations | mask, unmask | + SHA-1, base64, CRC-32C, UTF-8, findHeader |
209+
| NT stores | No | Auto for payloads > 50% L3 |
210+
| Portability | Any platform with C compiler | Linux x86-64 (C fallback elsewhere) |
142211

143-
## Relevant to Meteor/DDP
212+
## CPU feature detection
144213

145-
If you're running Meteor.js, every DDP message (method calls, subscriptions, collection updates) goes through WebSocket framing. On a busy system with hundreds of concurrent connections, the masking loop runs thousands of times per second. Shaving microseconds here compounds into meaningful CPU savings.
214+
At module load, `_init_cpu_features()` runs CPUID to detect:
146215

147-
To integrate with Meteor's internal WebSocket handling:
216+
- **cpu_tier**: 0 (scalar), 1 (SSE2), 2 (AVX2), 3 (AVX-512F+BW)
217+
- **cpu_features**: GFNI, PCLMULQDQ, BMI2, LZCNT, VBMI, VBMI2, AMD vendor
218+
- **nt_threshold**: 50% of L3 cache size (auto-detected via cache topology CPUID leaves)
148219

149-
```javascript
150-
// server/startup.js
151-
// Meteor uses sockjs by default, but if using raw ws:
152-
import { WebApp } from "meteor/webapp";
220+
All dispatch is runtime — a single binary adapts to the host CPU.
153221

154-
// The ws package inside Meteor will pick up bufferutil
155-
// if it's in node_modules. Use the package aliasing approach.
156-
```
222+
## Relevant to Meteor/DDP
223+
224+
If you're running Meteor.js, every DDP message (method calls, subscriptions, collection updates) goes through WebSocket framing. On a busy system with hundreds of concurrent connections, the masking loop runs thousands of times per second. Shaving microseconds here compounds into meaningful CPU savings.
157225

158226
## File structure
159227

160228
```
161229
asm-bufferutil/
162230
├── src/
163-
│ ├── ws_napi.c # N-API C glue layer
231+
│ ├── ws_fast_api.cc # Direct V8 C++ API bridge
232+
│ ├── ws_napi.c # Legacy N-API bridge
164233
│ ├── ws_sha1_ni.c # SHA-1 with Intel SHA-NI intrinsics
165-
│ ├── ws_mask_asm.asm # XOR masking (SSE2/NT-store dispatch)
166-
│ ├── ws_base64_asm.asm # Base64 (AVX2/GFNI/SSE2/scalar dispatch)
167-
│ ├── ws_cpu.asm # CPU feature detection + tier bitmask
168-
│ ├── ws_crc32_asm.asm # CRC32 (SSE4.2)
234+
│ ├── ws_fallback.c # C/SIMD fallback (Windows/macOS)
235+
│ ├── ws_mask_asm.asm # Mask/unmask + findHeader (AVX-512/AVX2/SSE2/GPR)
236+
│ ├── ws_base64_asm.asm # Base64 encode (VBMI2/VBMI/AVX2/SSE2/scalar)
237+
│ ├── ws_crc32_asm.asm # CRC-32C (PCLMULQDQ folding / SSE4.2)
238+
│ ├── ws_utf8_asm.asm # UTF-8 validation (AVX-512/AVX2 ASCII + scalar)
239+
│ ├── ws_cpu.asm # CPUID feature detection + cache topology
169240
│ └── websocket_server.asm # WebSocket server-side frame assembly
170241
├── test/
171-
│ ├── index.js # Correctness test suite
172-
│ ├── crc32.c # C-level CRC32 harness
242+
│ ├── index.js # Correctness test suite (89 tests)
243+
│ ├── crc32.c # C-level CRC-32C test harness
173244
│ └── client.html # Browser WebSocket test client
174245
├── bench/
175-
│ └── index.js # Throughput benchmarks
176-
├── .github/workflows/ci.yml
177-
├── binding.gyp # node-gyp build config
246+
│ └── index.js # Time-based benchmarks (all operations)
247+
├── .github/workflows/
248+
│ ├── ci.yml # CI: build + test
249+
│ └── benchmark.yml # Benchmark with artifact upload
250+
├── binding.gyp # node-gyp build config (NASM actions)
178251
├── index.js # JS entry point with native/fallback dispatch
179252
├── package.json
180253
├── CHANGELOG.md

0 commit comments

Comments
 (0)