jz 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -72
- package/bench/README.md +253 -100
- package/bench/bench.svg +58 -81
- package/cli.js +85 -12
- package/dist/interop.js +1 -0
- package/dist/jz.js +6196 -0
- package/index.d.ts +126 -0
- package/index.js +222 -35
- package/interop.js +240 -141
- package/layout.js +34 -18
- package/module/array.js +99 -111
- package/module/collection.js +124 -30
- package/module/console.js +1 -1
- package/module/core.js +163 -19
- package/module/json.js +3 -3
- package/module/math.js +162 -3
- package/module/number.js +268 -13
- package/module/object.js +37 -5
- package/module/regex.js +8 -7
- package/module/simd.js +37 -5
- package/module/string.js +203 -168
- package/module/typedarray.js +565 -112
- package/package.json +26 -7
- package/src/abi/string.js +29 -29
- package/src/ast.js +19 -2
- package/src/autoload.js +3 -0
- package/src/compile/analyze-scans.js +174 -11
- package/src/compile/analyze.js +101 -4
- package/src/compile/cse-load.js +200 -0
- package/src/compile/emit-assign.js +82 -20
- package/src/compile/emit.js +592 -51
- package/src/compile/index.js +504 -89
- package/src/compile/infer.js +36 -1
- package/src/compile/loop-divmod.js +109 -0
- package/src/compile/loop-model.js +91 -0
- package/src/compile/loop-square.js +102 -0
- package/src/compile/narrow.js +275 -41
- package/src/compile/peel-stencil.js +215 -0
- package/src/compile/plan/advise.js +55 -1
- package/src/compile/plan/common.js +29 -0
- package/src/compile/plan/index.js +8 -1
- package/src/compile/plan/inline.js +180 -22
- package/src/compile/plan/literals.js +313 -24
- package/src/compile/plan/scope.js +115 -39
- package/src/compile/program-facts.js +21 -2
- package/src/ctx.js +96 -19
- package/src/helper-counters.js +137 -0
- package/src/ir.js +157 -20
- package/src/kind-traits.js +34 -3
- package/src/kind.js +79 -4
- package/src/op-policy.js +5 -2
- package/src/ops.js +119 -0
- package/src/optimize/index.js +1274 -151
- package/src/optimize/recurse.js +182 -0
- package/src/optimize/vectorize.js +4187 -253
- package/src/prepare/index.js +75 -14
- package/src/prepare/lift-iife.js +149 -0
- package/src/reps.js +6 -2
- package/src/static.js +9 -0
- package/src/type.js +63 -51
- package/src/wat/assemble.js +286 -21
- package/src/widen.js +21 -0
- package/src/wat/optimize.js +0 -3760
package/bench/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# JZ benchmark suite
|
|
2
2
|
|
|
3
|
-
Cross-target workload suite for
|
|
3
|
+
Cross-target workload suite for JZ codegen quality. Each benchmark is a case
|
|
4
4
|
folder under `bench/`:
|
|
5
5
|
|
|
6
6
|
```txt
|
|
@@ -10,7 +10,6 @@ bench/<case>/<case>.rs optional Rust baseline
|
|
|
10
10
|
bench/<case>/<case>.go optional Go baseline
|
|
11
11
|
bench/<case>/<case>.zig optional Zig baseline
|
|
12
12
|
bench/<case>/<case>.as.ts optional AssemblyScript baseline
|
|
13
|
-
bench/<case>/<case>.py optional scalar CPython baseline
|
|
14
13
|
bench/<case>/<case>.npy.py optional NumPy baseline
|
|
15
14
|
bench/<case>/<case>.wat optional hand-written WAT baseline
|
|
16
15
|
```
|
|
@@ -30,8 +29,8 @@ drift as `DIFF`.
|
|
|
30
29
|
npm run bench
|
|
31
30
|
node bench/bench.mjs --targets=nat,rust,go,numpy,v8,jz
|
|
32
31
|
node bench/bench.mjs --targets=jz --cases=biquad,mat4,poly,bitwise
|
|
33
|
-
node bench/bench.mjs --targets=v8,deno,bun,spidermonkey,
|
|
34
|
-
node bench/bench.mjs --cases=biquad,
|
|
32
|
+
node bench/bench.mjs --targets=v8,deno,bun,spidermonkey,graaljs
|
|
33
|
+
node bench/bench.mjs --cases=biquad,fft,synth,bytebeat,blur
|
|
35
34
|
node bench/bench.mjs biquad
|
|
36
35
|
node bench/bench.mjs mat4 --targets=nat,v8,jz
|
|
37
36
|
```
|
|
@@ -51,22 +50,80 @@ node bench/bench.mjs mat4 --targets=nat,v8,jz
|
|
|
51
50
|
| [`json`](json/json.js) | runtime `JSON.parse` of one module-local source plus heterogeneous object/array walk with a stable inferred JSON shape |
|
|
52
51
|
| [`sort`](sort/sort.js) | in-place heapsort over a typed array; exposes call-heavy nested loops and typed-array index propagation |
|
|
53
52
|
| [`crc32`](crc32/crc32.js) | table-driven CRC-32 over a mutable byte buffer; exposes integer narrowing and typed-array parameter propagation |
|
|
53
|
+
| [`dotprod`](dotprod/dotprod.js) | multiply-accumulate reduction; JZ reassociates to 4 SIMD accumulators, beating strict-fp serial native |
|
|
54
|
+
| [`bytebeat`](bytebeat/bytebeat.js) | classic integer "bytebeat" one-line-song synthesis to 8-bit PCM; pure i32, bit-exact everywhere |
|
|
55
|
+
| [`fft`](fft/fft.js) | radix-2 Cooley–Tukey FFT over a transcendental-free twiddle table; the canonical numeric/audio kernel |
|
|
56
|
+
| [`synth`](synth/synth.js) | minisynth audio pipeline — polynomial oscillator + ADSR + biquad per sample; loop-carried f64 |
|
|
57
|
+
| [`blur`](blur/blur.js) | separable box blur on an RGBA8 image; integer stencil with edge clamp, the canonical image pipeline |
|
|
58
|
+
| [`conv2d`](conv2d/conv2d.js) | int8 quantized conv layer (Cin→Cout, 3×3, i32 MAC + ReLU requant); the spatial-convolution counterpart to dense `matmul`, the edge-ML inference kernel |
|
|
59
|
+
| [`qoi`](qoi/qoi.js) | QOI lossless image codec encode + decode round-trip; loop-carried run/index/diff/luma ops that no target can vectorize — pure scalar-codegen race |
|
|
60
|
+
| [`hash`](hash/hash.js) | MurmurHash3 x86_32 over a byte buffer; table-free multiply/rotate/xor mixing, a different ALU profile from `crc32` |
|
|
61
|
+
| [`lz`](lz/lz.js) | LZSS compress + inflate round-trip; greedy sliding-window longest-match search — the match finder that dominates every LZ-family codec |
|
|
62
|
+
| [`base64`](base64/base64.js) | Base64 encode + decode round-trip; table-driven 3→4 byte packing, the canonical codec / serialization kernel |
|
|
63
|
+
| [`wav`](wav/wav.js) | PCM-16 WAV encoder; per-sample clamp + int16 quantization behind a RIFF/WAVE header, the audio serialization kernel |
|
|
64
|
+
| [`raytrace`](raytrace/raytrace.js) | minimal sphere ray tracer — primary ray + closest-hit + Lambert/ambient shading to an f64 framebuffer; the canonical 3-D render kernel, a branchy loop-carried scalar `sqrt` pipeline no target vectorizes |
|
|
65
|
+
| [`noise`](noise/noise.js) | 2-D Perlin gradient noise summed over 5 fBm octaves; the canonical procedural-generation kernel — integer permutation-table hashing interleaved with f64 smoothstep interpolation |
|
|
66
|
+
| [`radixsort`](radixsort/radixsort.js) | LSD radix sort (4 × 8-bit counting passes) over u32 keys; histogram → prefix-sum → scatter with buffer ping-pong, the gather/scatter integer-sort counterpart to compare-swap `sort` |
|
|
67
|
+
| [`levenshtein`](levenshtein/levenshtein.js) | Levenshtein edit-distance rolling-row dynamic program; the canonical sequence-alignment / fuzzy-match kernel — a branch- and min-reduction-heavy DP with a diagonal data dependency no target vectorizes |
|
|
68
|
+
| [`nqueens`](nqueens/nqueens.js) | bitmask N-Queens solution counter (board sizes drawn from a runtime array so the recursion can't be constant-folded); deep backtracking recursion + per-node bitmask branch — the call/recursion-codegen probe |
|
|
69
|
+
| [`dict`](dict/dict.js) | open-addressing hash table (build + probe) with linear probing; multiply-shift hash scatter + dependent-load gather + unpredictable probe-chain branches — the associative-container kernel |
|
|
70
|
+
| [`sieve`](sieve/sieve.js) | Sieve of Eratosthenes over a byte array; pure strided scatter (j = i², i²+i, …) guarded by an outer branch — a non-contiguous write pattern |
|
|
71
|
+
| [`vm`](vm/vm.js) | tiny bytecode interpreter — a fetch-decode-dispatch loop (if/else opcode chain + indirect code-array loads) running an integer recurrence; the canonical VM/regex-engine inner loop |
|
|
72
|
+
| [`spmv`](spmv/spmv.js) | sparse matrix×vector in CSR form; the MAC inner loop gathers `x[col[k]]` through a column-index array — the data-dependent gather dense codegen handles worst (exact-integer f64, bit-identical everywhere) |
|
|
73
|
+
| [`hashjoin`](hashjoin/hashjoin.js) | probe-dominated relational hash join (build small, stream a large probe side, sum matched payloads) — the database/dataframe kernel and JZ's boss case: `probe()` returns a typed-array element through a param JZ can't yet prove is i32, so `sum + probe()` lowers to a polymorphic number-or-string add per probe — the only case JZ trails V8; proving the return i32 measured 8.7 → 5.2 ms, jz then beats V8 1.3× |
|
|
54
74
|
| [`watr`](watr/watr.js) | watr's WAT-to-wasm compiler on a small WAT corpus; compares jz-compiled compiler code with raw V8 |
|
|
55
75
|
| [`jessie`](jessie/jessie.js) | the subscript/jessie JS parser over a realistic source corpus; branch-, allocation- and recursion-heavy front-end work |
|
|
56
|
-
| [`jz`](jz/jz.js) | the
|
|
76
|
+
| [`jz`](jz/jz.js) | the JZ compiler itself (scripts/self.js pipeline) compiling three small programs at L2 — the self-host row runs jz.wasm compiling JavaScript; output bytes are checksummed so the parity gate doubles as a determinism proof |
|
|
77
|
+
|
|
78
|
+
The `watr`, `jessie`, and `jz` rows are self-referential — JZ (or its deps)
|
|
79
|
+
compiling code. They stay runnable (`--cases=watr,jessie,jz`) and gated in
|
|
80
|
+
`test/bench.js`, but are hidden from the bench page and the headline geomean SVG:
|
|
81
|
+
they answer a different question (compiler throughput on itself) from the
|
|
82
|
+
cross-language kernel comparison the page is about.
|
|
57
83
|
|
|
58
84
|
Native rows for `json` are fixed-source references, not semantic equivalents
|
|
59
85
|
of JavaScript `JSON.parse`: C/Rust/Zig hand-parse the known schema from a
|
|
60
86
|
compile-time string, and Zig may constant-fold the whole parse+walk under
|
|
61
87
|
ReleaseFast; Go uses `encoding/json` but still unmarshals the same compile-time
|
|
62
|
-
string. The
|
|
88
|
+
string. The JZ row parses a `let` source at runtime so `JSON.parse` is not
|
|
63
89
|
compile-time folded, while the compiler can still specialize the stable literal
|
|
64
90
|
shape. External unknown-shape JSON still uses the generic runtime parser.
|
|
65
91
|
|
|
66
92
|
Native-language rows are intentionally per case. NumPy rows are used only
|
|
67
|
-
where a vectorized array implementation is a meaningful Python convention
|
|
68
|
-
|
|
69
|
-
|
|
93
|
+
where a vectorized array implementation is a meaningful Python convention.
|
|
94
|
+
|
|
95
|
+
Pure interpreters (CPython, QuickJS, Hermes) stay out of the **headline**
|
|
96
|
+
geomean: it measures compiled-code quality, and an interpreter row inflates JZ's
|
|
97
|
+
lead for free without answering a question a user actually has. Javy
|
|
98
|
+
(embedded-QuickJS) is wired as a **fenced reference** — it appears per case as a
|
|
99
|
+
hatched row so the cost of the "ship a JS interpreter in wasm" approach is
|
|
100
|
+
visible, but it is excluded from `SVG_TARGETS` so it never moves the headline
|
|
101
|
+
number. The headline field is wasm-vs-wasm, apples-to-apples: JZ against the
|
|
102
|
+
other languages compiled to the same target — WebAssembly run in V8 — i.e. Rust,
|
|
103
|
+
Go, C, and Zig (`wasm32-wasi`), AssemblyScript, Porffor, and MoonBit (the last on
|
|
104
|
+
moonrun, MoonBit's V8 wasm runner), plus the JS JITs it replaces
|
|
105
|
+
(V8/Bun/Deno/SpiderMonkey/GraalJS). Native C/Rust/Zig/Go and NumPy's
|
|
106
|
+
vectorized C are the corpus reference band — the aggregate "what you give up by
|
|
107
|
+
not rewriting", with native C the speed-of-light ceiling.
|
|
108
|
+
|
|
109
|
+
**Coverage is reported, not hidden.** A target that is present but cannot compile
|
|
110
|
+
or run a case (Porffor OOMs on most typed-array kernels; TinyGo's stdlib subset
|
|
111
|
+
rejects some `.go`) records a `{ status: 'fail', reason }` entry in
|
|
112
|
+
`results.json`, and the page renders it as a muted row with the reason — and as a
|
|
113
|
+
`ran / attempted` count on the headline row. The geomean still averages only the
|
|
114
|
+
cases a target completed correctly (you cannot ratio a run that never produced a
|
|
115
|
+
number), but the gap is no longer invisible.
|
|
116
|
+
|
|
117
|
+
The **per-case** view splits into two **same-class lanes**, so every bar in a
|
|
118
|
+
card is a fair peer:
|
|
119
|
+
|
|
120
|
+
- **WASM** — JZ vs Rust/Go/C/Zig→wasm, AssemblyScript, Porffor, and raw JS, all
|
|
121
|
+
run in this V8.
|
|
122
|
+
- **Native** — JZ lowered to a native binary (`jz-w2c`: JZ wasm → `wasm2c` →
|
|
123
|
+
`clang -O3`) against the native toolchains (C/Rust/Go/Zig). This is the *fair*
|
|
124
|
+
native comparison — native-vs-native, not jz-wasm against a native binary — and
|
|
125
|
+
it's the axis for optimizing JZ's compile-to-native path. The lane hides where
|
|
126
|
+
no `jz-w2c` build is available (it needs wabt's wasm2c runtime + SIMDe headers).
|
|
70
127
|
|
|
71
128
|
### Parity classes
|
|
72
129
|
|
|
@@ -84,35 +141,39 @@ correctly-rounded; cascade is the same algorithm.
|
|
|
84
141
|
| --- | --- |
|
|
85
142
|
| `nat` | clang `-O3` native C baseline, when a matching C workload exists |
|
|
86
143
|
| `natgcc` | gcc `-O3`, when real gcc is installed |
|
|
87
|
-
| `rust` | Rust `rustc -C opt-level=3 -C target-cpu=native
|
|
88
|
-
| `go` | Go native compiler
|
|
89
|
-
| `zig` | Zig `build-exe -O ReleaseFast
|
|
90
|
-
| `python` | scalar CPython, when a matching `.py` exists |
|
|
144
|
+
| `rust` | Rust `rustc -C opt-level=3 -C target-cpu=native` — native baseline; the headline rival is `rust-wasm` |
|
|
145
|
+
| `go` | Go native compiler — native baseline; the headline rival is `go-wasm` |
|
|
146
|
+
| `zig` | Zig `build-exe -O ReleaseFast` — native baseline |
|
|
91
147
|
| `numpy` | vectorized NumPy, when a matching `.npy.py` exists |
|
|
92
148
|
| `v8` | raw JavaScript on Node/V8 |
|
|
93
149
|
| `deno` | raw JavaScript on Deno/V8 |
|
|
94
150
|
| `bun` | raw JavaScript on Bun/JavaScriptCore |
|
|
151
|
+
| `jsc` | raw JavaScript on the standalone JavaScriptCore shell — Safari's engine (`jsc`; install via `jsvu --engines=javascriptcore`, or set `JSC_BIN`) |
|
|
95
152
|
| `spidermonkey` | raw JavaScript on SpiderMonkey shell (`spidermonkey`, `sm`, `js128`, `js115`, `js102`, or `js`) |
|
|
96
|
-
| `hermes` | raw JavaScript on Hermes |
|
|
97
153
|
| `shermes` | Static Hermes — JS AOT-compiled to a native binary (`shermes -O`), when installed |
|
|
98
154
|
| `graaljs` | raw JavaScript on GraalJS |
|
|
99
|
-
| `jz` |
|
|
155
|
+
| `jz` | JZ output with host imports for timing/logging (measures wasm size without WASI console/perf bloat) |
|
|
100
156
|
| `as` | AssemblyScript `asc -O3 --runtime stub`, when a matching `.as.ts` exists |
|
|
101
|
-
| `
|
|
102
|
-
| `
|
|
157
|
+
| `rust-wasm` | Rust → `wasm32-wasip1` (`rustc --target wasm32-wasip1 -C opt-level=3`), run in node's V8 — the apples-to-apples Rust rival |
|
|
158
|
+
| `go-wasm` | Go → `wasm32-wasip1` (`GOOS=wasip1 GOARCH=wasm go build`), run in node's V8 |
|
|
159
|
+
| `c-wasm` | C → `wasm32-wasi` via clang/LLVM (`zig cc -target wasm32-wasi -O3` — zig supplies the wasi-libc that plain clang lacks; no emcc/wasi-sdk install), run in node's V8 |
|
|
160
|
+
| `jz-wasmtime` | JZ output on wasmtime |
|
|
161
|
+
| `jz-w2c` | JZ wasm translated by wabt `wasm2c`, then clang `-O3` |
|
|
103
162
|
| `wat` | hand-written WAT baseline when a case provides `run-wat.mjs` |
|
|
104
|
-
| `qjs` | QuickJS when installed |
|
|
105
163
|
| `porf` | Porffor (`porf run`) when installed |
|
|
106
|
-
| `jawsm` | jawsm when installed |
|
|
164
|
+
| `jawsm` | jawsm (JS → WasmGC) when installed |
|
|
165
|
+
| `javy` | Javy (`javy build` — JS in embedded QuickJS) when installed — fenced interpreter reference, never in the headline geomean |
|
|
166
|
+
| `tinygo` | TinyGo → `wasm32-wasip1` (`tinygo build -target=wasip1 -opt=2`) — the Go corpus through LLVM, leaner wasm than `go-wasm`; run in node's V8 |
|
|
167
|
+
| `moonbit` | MoonBit → `wasm` (`moon build --target wasm --release`), run on `moonrun` (MoonBit's V8 wasm runner, which supplies the monotonic clock) — a wasm-first-language rival, when `moon`/`moonrun` are installed |
|
|
107
168
|
|
|
108
169
|
The `size` column reports the artifact size each target measures: the
|
|
109
170
|
compiled native binary for `nat`/`rust`/`go`/`zig`, the produced
|
|
110
|
-
`.wasm` for `jz`/`as`/hand-WAT/jawsm/`jz-w2c` (the C-translated
|
|
171
|
+
`.wasm` for `jz`/`as`/`rust-wasm`/`go-wasm`/`c-wasm`/hand-WAT/jawsm/`jz-w2c` (the C-translated
|
|
111
172
|
executable), or the source file for raw-JS interpreters where there is no
|
|
112
173
|
compile step. For source files with imports, raw-JS size is only the entry file;
|
|
113
|
-
|
|
174
|
+
JZ size is the bundled wasm artifact.
|
|
114
175
|
|
|
115
|
-
Note the preset trade: these speed tables build
|
|
176
|
+
Note the preset trade: these speed tables build JZ at the default
|
|
116
177
|
(`optimize: 2`, speed-leaning — loop unroll + SIMD lift), while the dedicated
|
|
117
178
|
size comparison (`npm run bench:size`) builds with `optimize: 'size'` —
|
|
118
179
|
typically ~2× smaller on the same kernel (biquad: 3.5 kB default vs 1.6 kB
|
|
@@ -121,40 +182,40 @@ same artifact.
|
|
|
121
182
|
|
|
122
183
|
Runtime command overrides:
|
|
123
184
|
|
|
124
|
-
`watr` is intentionally compiled by
|
|
185
|
+
`watr` is intentionally compiled by JZ with a size-oriented pass config
|
|
125
186
|
(`watr:false`, `smallConstForUnroll:false`): on a large compiler bundle, the
|
|
126
187
|
default WAT-level optimizer and small-loop unroll grow code more than they help.
|
|
127
|
-
This keeps the target measuring the best current
|
|
188
|
+
This keeps the target measuring the best current JZ artifact for that workload.
|
|
128
189
|
|
|
129
190
|
```sh
|
|
130
191
|
BUN_BIN=/path/to/bun \
|
|
131
192
|
DENO_BIN=/path/to/deno \
|
|
132
193
|
SPIDERMONKEY_BIN=/path/to/js \
|
|
133
|
-
HERMES_BIN=/path/to/hermes \
|
|
134
194
|
SHERMES_BIN=/path/to/shermes \
|
|
135
195
|
GRAALJS_BIN=/path/to/graaljs \
|
|
136
196
|
PORF_BIN=/path/to/porf \
|
|
137
|
-
node bench/bench.mjs --targets=bun,deno,spidermonkey,
|
|
197
|
+
node bench/bench.mjs --targets=bun,deno,spidermonkey,shermes,graaljs,porf
|
|
138
198
|
```
|
|
139
199
|
|
|
140
200
|
## Reading the numbers (darwin/arm64, M-class)
|
|
141
201
|
|
|
142
|
-
Snapshots from `node bench/bench.mjs --targets=v8,jz,as
|
|
143
|
-
Where
|
|
144
|
-
(`as`) is the headline comparison
|
|
145
|
-
|
|
202
|
+
Snapshots from `node bench/bench.mjs --targets=v8,jz,as` (the WASM lane).
|
|
203
|
+
Where JZ lands relative to **V8 raw JS** (`v8/node`) and **AssemblyScript**
|
|
204
|
+
(`as`) is the headline comparison; the hand-WAT row is the wasm-in-V8 floor.
|
|
205
|
+
The fair native comparison lives in the page's separate **Native lane** (`jz-w2c`
|
|
206
|
+
vs the native toolchains), not in these wasm snapshots — a native binary against
|
|
207
|
+
wasm-in-V8 would be a wrong-class single-case compare.
|
|
146
208
|
|
|
147
209
|
### biquad — f64 typed-array DSP cascade
|
|
148
210
|
|
|
149
211
|
| target | median | ×v8 | size | parity |
|
|
150
212
|
| --- | ---: | ---: | ---: | --- |
|
|
151
|
-
| **
|
|
152
|
-
| AssemblyScript (asc -O3 --runtime stub) |
|
|
153
|
-
| V8 (node) raw JS |
|
|
154
|
-
| native C (clang -O3) | 5.32 ms | 2.32× | 32.7 kB | ok |
|
|
213
|
+
| **JZ → V8 wasm** | **4.64 ms** | **1.94×** | **3.4 kB** | **ok** |
|
|
214
|
+
| AssemblyScript (asc -O3 --runtime stub) | 6.51 ms | 1.38× | 1.9 kB | ok |
|
|
215
|
+
| V8 (node) raw JS | 8.98 ms | 1.00× | 3.2 kB | ok |
|
|
155
216
|
| hand-WAT → V8 wasm | 6.49 ms | 1.90× | 767 B | ok |
|
|
156
217
|
|
|
157
|
-
|
|
218
|
+
JZ beats V8 raw JS by 2.1× and AS by 1.4×. The typed-array scalarization,
|
|
158
219
|
offset-fusion, and base-hoisting pipeline delivers dense-f64 loop codegen
|
|
159
220
|
that matches the hand-WAT floor.
|
|
160
221
|
|
|
@@ -162,13 +223,12 @@ that matches the hand-WAT floor.
|
|
|
162
223
|
|
|
163
224
|
| target | median | ×v8 | size | parity |
|
|
164
225
|
| --- | ---: | ---: | ---: | --- |
|
|
165
|
-
| **
|
|
166
|
-
| AssemblyScript (asc -O3 --runtime stub) |
|
|
167
|
-
| V8 (node) raw JS |
|
|
168
|
-
| native C (clang -O3) | 2.76 ms | 4.33× | 32.8 kB | ok |
|
|
226
|
+
| **JZ → V8 wasm** | **1.49 ms** | **5.75×** | **3.1 kB** | **ok** |
|
|
227
|
+
| AssemblyScript (asc -O3 --runtime stub) | 6.71 ms | 1.28× | 1.6 kB | ok |
|
|
228
|
+
| V8 (node) raw JS | 8.57 ms | 1.00× | 1.2 kB | ok |
|
|
169
229
|
| hand-WAT → V8 wasm | 8.12 ms | 1.47× | 414 B | ok |
|
|
170
230
|
|
|
171
|
-
|
|
231
|
+
JZ is 5.9× faster than V8 raw JS and 4.6× faster than AS. The scalarized
|
|
172
232
|
SIMD hot path (unrolled 4×4 multiply) is the win; V8's JIT doesn't vectorize
|
|
173
233
|
this from JS source.
|
|
174
234
|
|
|
@@ -176,11 +236,11 @@ this from JS source.
|
|
|
176
236
|
|
|
177
237
|
| target | median | ×v8 | size | parity |
|
|
178
238
|
| --- | ---: | ---: | ---: | --- |
|
|
179
|
-
| **
|
|
180
|
-
| AssemblyScript (asc -O3 --runtime stub) |
|
|
181
|
-
| V8 (node) raw JS |
|
|
239
|
+
| **JZ → V8 wasm** | **0.13 ms** | **12.67×** | **1.4 kB** | **ok** |
|
|
240
|
+
| AssemblyScript (asc -O3 --runtime stub) | 0.81 ms | 2.07× | 1.3 kB | ok |
|
|
241
|
+
| V8 (node) raw JS | 1.67 ms | 1.00× | 1014 B | ok |
|
|
182
242
|
|
|
183
|
-
|
|
243
|
+
JZ is 12.9× faster than V8 raw JS and 5.9× faster than AS. The bimorphic
|
|
184
244
|
`sum` (called with both `Float64Array` and `Int32Array`) stays on typed
|
|
185
245
|
paths without falling back to generic dispatch.
|
|
186
246
|
|
|
@@ -188,13 +248,12 @@ paths without falling back to generic dispatch.
|
|
|
188
248
|
|
|
189
249
|
| target | median | ×v8 | size | parity |
|
|
190
250
|
| --- | ---: | ---: | ---: | --- |
|
|
191
|
-
| **
|
|
192
|
-
| V8 (node) raw JS |
|
|
193
|
-
| AssemblyScript (asc -O3 --runtime stub) |
|
|
194
|
-
|
|
|
195
|
-
| hand-WAT → V8 wasm | 4.88 ms | 1.09× | 355 B | ok |
|
|
251
|
+
| **JZ → V8 wasm** | **1.01 ms** | **3.84×** | **1.2 kB** | **ok** |
|
|
252
|
+
| V8 (node) raw JS | 3.87 ms | 1.00× | 1005 B | ok |
|
|
253
|
+
| AssemblyScript (asc -O3 --runtime stub) | 9.15 ms | 0.42× | 1.5 kB | ok |
|
|
254
|
+
| hand-WAT → V8 wasm | 3.59 ms | 1.11× | 355 B | ok |
|
|
196
255
|
|
|
197
|
-
|
|
256
|
+
JZ is 4.0× faster than V8 raw JS and 8.8× faster than AS. The i32 hot path
|
|
198
257
|
(`Math.imul`, `|0`, `>>>0`) now lowers to raw `i32` ops without NaN-box
|
|
199
258
|
overhead on every operation.
|
|
200
259
|
|
|
@@ -202,24 +261,22 @@ overhead on every operation.
|
|
|
202
261
|
|
|
203
262
|
| target | median | ×v8 | size | parity |
|
|
204
263
|
| --- | ---: | ---: | ---: | --- |
|
|
205
|
-
|
|
|
206
|
-
|
|
|
207
|
-
| V8 (node) raw JS | 0.
|
|
264
|
+
| **JZ → V8 wasm** | **0.06 ms** | **2.26×** | **2.2 kB** | **ok** |
|
|
265
|
+
| AssemblyScript (asc -O3 --runtime stub) | 0.06 ms | 2.15× | 1.6 kB | ok |
|
|
266
|
+
| V8 (node) raw JS | 0.13 ms | 1.00× | 2.0 kB | ok |
|
|
208
267
|
|
|
209
|
-
|
|
210
|
-
`
|
|
211
|
-
jz still boxes the string pointer. The gap is narrow (0.02 ms) and both
|
|
212
|
-
are well ahead of V8.
|
|
268
|
+
JZ is 2.4× faster than V8 raw JS and now edges out AS by ~1.2× on this
|
|
269
|
+
`charCodeAt`-heavy scan. Both are well ahead of V8.
|
|
213
270
|
|
|
214
271
|
### callback — `Array.map` closure + i32 fold
|
|
215
272
|
|
|
216
273
|
| target | median | ×v8 | size | parity |
|
|
217
274
|
| --- | ---: | ---: | ---: | --- |
|
|
218
|
-
| **
|
|
219
|
-
|
|
|
220
|
-
|
|
|
275
|
+
| **JZ → V8 wasm** | **0.26 ms** | **2.37×** | **1.4 kB** | **ok** |
|
|
276
|
+
| V8 (node) raw JS | 0.62 ms | 1.00× | 1.3 kB | ok |
|
|
277
|
+
| AssemblyScript (asc -O3 --runtime stub) | 0.80 ms | 0.78× | 1.9 kB | ok |
|
|
221
278
|
|
|
222
|
-
|
|
279
|
+
JZ is 2.3× faster than V8 raw JS and 2.9× faster than AS. Closure +
|
|
223
280
|
`Array.map` lowers to a preallocated typed loop with no per-iteration alloc.
|
|
224
281
|
V8's JIT does not inline the closure across the `map` boundary.
|
|
225
282
|
|
|
@@ -227,11 +284,11 @@ V8's JIT does not inline the closure across the `map` boundary.
|
|
|
227
284
|
|
|
228
285
|
| target | median | ×v8 | size | parity |
|
|
229
286
|
| --- | ---: | ---: | ---: | --- |
|
|
230
|
-
| **
|
|
231
|
-
| V8 (node) raw JS | 1.
|
|
232
|
-
| AssemblyScript (asc -O3 --runtime stub) | 1.
|
|
287
|
+
| **JZ → V8 wasm** | **0.69 ms** | **1.93×** | **1.8 kB** | **ok** |
|
|
288
|
+
| V8 (node) raw JS | 1.33 ms | 1.00× | 1.1 kB | ok |
|
|
289
|
+
| AssemblyScript (asc -O3 --runtime stub) | 1.40 ms | 0.95× | 2.1 kB | ok |
|
|
233
290
|
|
|
234
|
-
|
|
291
|
+
JZ is 1.9× faster than V8 raw JS and 2.0× faster than AS. Schema-slot
|
|
235
292
|
reads are direct field offsets; the gap is small because the workload is
|
|
236
293
|
memory-bound.
|
|
237
294
|
|
|
@@ -239,81 +296,177 @@ memory-bound.
|
|
|
239
296
|
|
|
240
297
|
| target | median | ×v8 | size | parity |
|
|
241
298
|
| --- | ---: | ---: | ---: | --- |
|
|
242
|
-
| AssemblyScript (asc -O3 --runtime stub) |
|
|
243
|
-
| **
|
|
244
|
-
| V8 (node) raw JS |
|
|
299
|
+
| AssemblyScript (asc -O3 --runtime stub) | 8.90 ms | 1.11× | 1.3 kB | ok |
|
|
300
|
+
| **JZ → V8 wasm** | **8.45 ms** | **1.17×** | **1.4 kB** | **ok** |
|
|
301
|
+
| V8 (node) raw JS | 9.90 ms | 1.00× | 1.8 kB | ok |
|
|
245
302
|
|
|
246
|
-
|
|
303
|
+
JZ is 1.1× faster than V8 raw JS and ties AS. The dense f64 hot loop with
|
|
247
304
|
conditional break compacts to 1.0 kB — the smallest wasm in the suite.
|
|
248
305
|
|
|
249
306
|
### json — runtime `JSON.parse` plus stable-shape walk
|
|
250
307
|
|
|
251
308
|
| target | median | ×v8 | size | parity |
|
|
252
309
|
| --- | ---: | ---: | ---: | --- |
|
|
253
|
-
| **
|
|
254
|
-
| V8 (node) raw JS | 0.
|
|
310
|
+
| **JZ → V8 wasm** | **0.21 ms** | **1.28×** | **9.9 kB** | **ok** |
|
|
311
|
+
| V8 (node) raw JS | 0.27 ms | 1.00× | 1.2 kB | ok |
|
|
255
312
|
|
|
256
|
-
|
|
313
|
+
JZ is 1.3× faster than V8 raw JS. The runtime parser is specialized to the
|
|
257
314
|
inferred JSON shape; AS is skipped because it cannot parse JSON at runtime.
|
|
258
315
|
|
|
259
316
|
### sort — in-place heapsort over typed array
|
|
260
317
|
|
|
261
318
|
| target | median | ×v8 | size | parity |
|
|
262
319
|
| --- | ---: | ---: | ---: | --- |
|
|
263
|
-
| **
|
|
264
|
-
| AssemblyScript (asc -O3 --runtime stub) |
|
|
265
|
-
| V8 (node) raw JS |
|
|
320
|
+
| **JZ → V8 wasm** | **4.65 ms** | **1.67×** | **1.8 kB** | **ok** |
|
|
321
|
+
| AssemblyScript (asc -O3 --runtime stub) | 7.96 ms | 0.98× | 1.9 kB | ok |
|
|
322
|
+
| V8 (node) raw JS | 7.79 ms | 1.00× | 1.6 kB | ok |
|
|
266
323
|
|
|
267
|
-
|
|
324
|
+
JZ is 1.6× faster than V8 raw JS and 1.4× faster than AS. Call-heavy
|
|
268
325
|
nested loops with typed-array index propagation stay on the i32 path.
|
|
269
326
|
|
|
270
327
|
### crc32 — table-driven CRC-32 over byte buffer
|
|
271
328
|
|
|
272
329
|
| target | median | ×v8 | size | parity |
|
|
273
330
|
| --- | ---: | ---: | ---: | --- |
|
|
274
|
-
| **
|
|
275
|
-
| AssemblyScript (asc -O3 --runtime stub) |
|
|
276
|
-
| V8 (node) raw JS |
|
|
331
|
+
| **JZ → V8 wasm** | **8.74 ms** | **1.11×** | **1.5 kB** | **ok** |
|
|
332
|
+
| AssemblyScript (asc -O3 --runtime stub) | 8.76 ms | 1.11× | 1.4 kB | ok |
|
|
333
|
+
| V8 (node) raw JS | 9.71 ms | 1.00× | 1.8 kB | ok |
|
|
277
334
|
|
|
278
|
-
|
|
335
|
+
JZ is 1.2× faster than V8 raw JS and ties AS. Integer narrowing and
|
|
279
336
|
typed-array parameter propagation keep the LUT lookup on raw i32.
|
|
280
337
|
|
|
338
|
+
### Audio + image showcase (cross-language, bit-exact)
|
|
339
|
+
|
|
340
|
+
Four standard kernels added to make the audio story concrete. All are written so
|
|
341
|
+
the output is **bit-identical** across every engine and native target — integer
|
|
342
|
+
math (bytebeat, blur) or transcendental-free f64 (fft, synth: in-source Taylor
|
|
343
|
+
polynomials, not `Math.sin`, which differs per libm). Go's arm64 auto-FMA gives
|
|
344
|
+
the documented `fma` parity class on the f64 cases.
|
|
345
|
+
|
|
346
|
+
| case | JZ | vs V8 | vs AS | vs fastest native | JZ wasm |
|
|
347
|
+
| --- | ---: | ---: | ---: | --- | ---: |
|
|
348
|
+
| **synth** — osc + ADSR + biquad | **2.32 ms** | **1.33×** | **1.07×** | **beats all** (Rust 0.89×) | 2.0 kB |
|
|
349
|
+
| **fft** — radix-2 Cooley–Tukey | **1.14 ms** | **1.27×** | **1.13×** | **ties** (Rust 1.07×) | 2.3 kB |
|
|
350
|
+
| **blur** — RGBA box blur | **0.90 ms** | **9.4×** | **5.8×** | trails (native SIMDs the stencil, 1.5×) | 3.4 kB |
|
|
351
|
+
| **bytebeat** — integer one-liner | **0.67 ms** | **3.7×** | **5.4×** | trails (native vectorizes, 1.3×) | 1.5 kB |
|
|
352
|
+
|
|
353
|
+
The headline: JZ beats the JS field (V8, AssemblyScript) on **every** audio/image
|
|
354
|
+
case, **ties native on FFT**, and is the **fastest of all targets on the synth
|
|
355
|
+
pipeline** — its per-sample loop is loop-carried (oscillator phase + biquad
|
|
356
|
+
feedback), so native can't auto-vectorize it either, and JZ's tight scalar f64
|
|
357
|
+
codegen with no NaN-box overhead wins outright. The two stateless integer kernels
|
|
358
|
+
(bytebeat, blur) are where `clang`/`zig`/`rustc` auto-vectorize an
|
|
359
|
+
embarrassingly-parallel loop JZ emits as scalar — native is the floor there, but
|
|
360
|
+
JZ still beats the JS field by 3.7–9.4× there. (Numbers: darwin/arm64, Apple M4 Max; the live snapshot
|
|
361
|
+
is [results.json](results.json).)
|
|
362
|
+
|
|
363
|
+
### General-codegen kernels — 3-D render, procedural, sort, sequence DP
|
|
364
|
+
|
|
365
|
+
Four kernels added to probe JZ's **general** scalar/branch/gather-scatter codegen,
|
|
366
|
+
beyond the SIMD-friendly reductions and stencils above — the cases a graphics,
|
|
367
|
+
procedural, data, or text workload actually hits. All bit-identical across targets
|
|
368
|
+
(`raytrace`/`noise` are transcendental-free f64, so Go's arm64 auto-FMA is the
|
|
369
|
+
documented `fma` class; `radixsort`/`levenshtein` are pure integer, exact
|
|
370
|
+
everywhere).
|
|
371
|
+
|
|
372
|
+
| case | JZ | vs V8 | vs AS | vs fastest native | JZ wasm |
|
|
373
|
+
| --- | ---: | ---: | ---: | --- | ---: |
|
|
374
|
+
| **radixsort** — LSD u32 sort | **2.75 ms** | **1.21×** | **1.19×** | trails (clang 1.5×) | 1.6 kB |
|
|
375
|
+
| **noise** — Perlin fBm (5 oct) | **6.59 ms** | **1.36×** | 0.23× | trails (Rust 5.2×) | 8.0 kB |
|
|
376
|
+
| **raytrace** — sphere render | **2.00 ms** | **1.07×** | 0.89× | trails (Rust 1.9×) | 4.8 kB |
|
|
377
|
+
| **levenshtein** — edit-distance DP | ~6 ms | ~1.0× | 0.35× | trails (clang ~3×) | 7.5 kB |
|
|
378
|
+
|
|
379
|
+
These are the honest mixed bag — and the point. JZ beats raw V8 on three of four
|
|
380
|
+
(and the smallest wasm of the whole field on `radixsort`, 1.6 kB), but it only
|
|
381
|
+
beats AssemblyScript on `radixsort`; it **trails AS** on `noise` (~4.3×) and
|
|
382
|
+
`levenshtein` (~2.5–2.9×), and only **ties V8** on `levenshtein` (the median
|
|
383
|
+
straddles 1.0× run-to-run). The two AS gaps localize the next codegen work:
|
|
384
|
+
`noise`'s nested permutation-table indirection (`perm[perm[X]+Y]`) and
|
|
385
|
+
`levenshtein`'s branchy `min`-reduction DP over a rolling typed-array row are
|
|
386
|
+
exactly the scalar/gather shapes JZ does not yet lower as tightly as AS's
|
|
387
|
+
`asc -O3`. They sit out the curated regression gate (`test/bench.js`) — like
|
|
388
|
+
`heat`/`matmul`/`nbody`/`particle`/`lorenz` — until JZ closes the gap and they
|
|
389
|
+
ratchet in as wins.
|
|
390
|
+
|
|
391
|
+
### Control-flow & gather/scatter kernels — recursion, hashing, sieve, VM, sparse
|
|
392
|
+
|
|
393
|
+
A second probe set, aimed squarely at the patterns the first batch flagged —
|
|
394
|
+
recursion, branchy probe chains, scatter, and indirect gather — to map where JZ's
|
|
395
|
+
general (non-SIMD) codegen actually stands. All bit-identical across every target
|
|
396
|
+
(`spmv` is f64 over exact small integers, so even Go-native FMA agrees; `nqueens`
|
|
397
|
+
draws its board sizes from a runtime array so clang/rustc can't fold the recursion
|
|
398
|
+
to a constant).
|
|
399
|
+
|
|
400
|
+
| case | JZ | vs V8 | vs AS | vs native C | what it probes |
|
|
401
|
+
| --- | ---: | ---: | ---: | ---: | --- |
|
|
402
|
+
| **vm** — bytecode dispatch | **7.30 ms** | **1.55×** | **2.20×** | **1.13×** | if/else opcode dispatch + indirect fetch |
|
|
403
|
+
| **spmv** — sparse A·x (CSR) | **2.67 ms** | **1.78×** | **1.25×** | 0.90× | indirect gather `x[col[k]]` |
|
|
404
|
+
| **sieve** — Eratosthenes | **7.18 ms** | **1.53×** | **1.30×** | 0.80× | strided scatter + outer branch |
|
|
405
|
+
| **nqueens** — backtracking | **6.05 ms** | **1.29×** | 0.93× | 0.78× | deep recursion + bitmask branch |
|
|
406
|
+
| **dict** — hash table | 4.11 ms | 0.78× | 0.81× | 0.39× | hash scatter + probe-chain gather |
|
|
407
|
+
|
|
408
|
+
The split is sharp and informative. **JZ is excellent at dense dispatch and
|
|
409
|
+
in-cache gather**: it wins `vm` against the *entire* field — including native
|
|
410
|
+
`clang -O3` (the if/else interpreter loop lowers to tight branches with no
|
|
411
|
+
NaN-box overhead, where AS pays per-access bounds checks), and wins `spmv`/`sieve`
|
|
412
|
+
over the JS field while sitting near native. But it **loses `dict` to both V8
|
|
413
|
+
(1.28×) and AS (1.24×)** — open-addressing's hash-scatter + dependent-load probe
|
|
414
|
+
chain is the same gather/branch shape as `noise` and `levenshtein`, and the
|
|
415
|
+
clearest, most reproducible deficiency the suite has surfaced. `nqueens` is a
|
|
416
|
+
near-tie with AS on recursion. Together the nine new cases triangulate it: JZ's
|
|
417
|
+
gap to the AS/native frontier is concentrated in **scatter-heavy hashing and
|
|
418
|
+
dependent-gather / branchy-DP** kernels, not in dense scalar or dispatch loops.
|
|
419
|
+
Like the batch above, these stay bench-only (out of `test/bench.js`) until the gap
|
|
420
|
+
closes.
|
|
421
|
+
|
|
281
422
|
### watr — WAT-to-wasm compiler on small corpus
|
|
282
423
|
|
|
283
424
|
| target | median | ×v8 | size | parity |
|
|
284
425
|
| --- | ---: | ---: | ---: | --- |
|
|
285
|
-
| V8 (node) raw JS | 1.
|
|
286
|
-
| **
|
|
426
|
+
| V8 (node) raw JS | 1.38 ms | 1.00× | 2.6 kB | ok |
|
|
427
|
+
| **JZ → V8 wasm** | **1.17 ms** | **1.17×** | **238.4 kB** | **ok** |
|
|
287
428
|
|
|
288
|
-
|
|
429
|
+
JZ is 1.07× slower than V8 raw JS on this large compiler bundle. The size
|
|
289
430
|
(144 kB) is the full jz-compiled watr parser + encoder + optimizer; V8's JIT
|
|
290
431
|
has the advantage of profile-guided tiering on a long-running compiler.
|
|
291
432
|
|
|
292
433
|
### Where the gaps live
|
|
293
434
|
|
|
294
|
-
Aggregate geomean (
|
|
435
|
+
Aggregate geomean (JZ / target):
|
|
295
436
|
|
|
296
437
|
| target | speed | size |
|
|
297
438
|
| --- | ---: | ---: |
|
|
298
|
-
| V8 (node) | **0.
|
|
299
|
-
| AssemblyScript | **0.
|
|
300
|
-
|
|
301
|
-
|
|
439
|
+
| V8 (node) | **0.40×** | — |
|
|
440
|
+
| AssemblyScript | **0.36×** | **1.16×** |
|
|
441
|
+
|
|
442
|
+
JZ wins or ties V8 on every kernel case; the only V8 losses are the
|
|
443
|
+
self-hosting rows `watr` (1.07×) and `jessie` (1.28×). AS is beaten on
|
|
444
|
+
speed across the shared cases. On size JZ is ~1.1× AS (geomean 1.16×,
|
|
445
|
+
median 1.10×) — JZ wins on speed, AS on bytes.
|
|
302
446
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
447
|
+
Against the systems languages compiled to the same target — **WebAssembly, run in
|
|
448
|
+
V8** — JZ is **2.4× faster than C, 2.6× than Rust, and 4.8× than Go** (geomean).
|
|
449
|
+
That apples-to-apples wasm field is the headline chart above, and JZ holds
|
|
450
|
+
**geomean parity with native `clang -O3`** itself (1.11×) — the lone non-wasm
|
|
451
|
+
reference row, the speed-of-light ceiling, never a per-case beat-claim.
|
|
306
452
|
|
|
307
453
|
Case-by-case summary:
|
|
308
454
|
|
|
309
|
-
* **biquad, mat4, poly, bitwise, callback: large wins.**
|
|
310
|
-
|
|
455
|
+
* **biquad, mat4, poly, bitwise, callback: large wins.** JZ beats V8 by
|
|
456
|
+
2.0–12.9× and AS by 1.4–9.1×. Typed-array scalarization, i32 narrowing,
|
|
311
457
|
and closure lowering are the drivers.
|
|
312
|
-
* **tokenizer, aos, mandelbrot, sort, crc32: modest wins.**
|
|
313
|
-
1.1–2.
|
|
458
|
+
* **tokenizer, aos, mandelbrot, sort, crc32: modest wins.** JZ beats V8 by
|
|
459
|
+
1.1–2.4× and ties or beats AS. These are memory-bound or branch-heavy
|
|
314
460
|
workloads where codegen quality matters less than data layout.
|
|
315
|
-
* **json: solid win.**
|
|
461
|
+
* **json: solid win.** JZ beats V8 by 1.3× on runtime JSON parsing; AS
|
|
316
462
|
cannot run this case.
|
|
317
|
-
* **
|
|
318
|
-
|
|
319
|
-
|
|
463
|
+
* **alpha: the native floor.** JZ beats V8 2.0× and every wasm rival (Rust/C/Go
|
|
464
|
+
→ wasm), but trails the native-C reference ~14× (374 µs vs 26 µs) — alpha's hot
|
|
465
|
+
path is an unsigned i32 multiply. JZ already lifts it to 128-bit SIMD (an i16x8
|
|
466
|
+
widening byte-map); native C pulls ahead on width alone — 256-bit AVX2 with a
|
|
467
|
+
fused byte multiply-add, run as native code with no per-load bounds checks. The
|
|
468
|
+
residual is the wasm-v128-vs-AVX2 ceiling, not a missing pass. A known gap we
|
|
469
|
+
publish rather than hide.
|
|
470
|
+
* **watr: near parity.** JZ is 1.07× slower than V8 on a 144 kB compiler
|
|
471
|
+
bundle. It is one of two self-host rows (with jessie) where V8's
|
|
472
|
+
profile-guided JIT tiers beat JZ's AOT wasm.
|