jz 0.6.0 → 0.7.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 +66 -43
- package/bench/README.md +128 -78
- package/bench/bench.svg +32 -42
- package/cli.js +73 -7
- package/dist/interop.js +1 -0
- package/dist/jz.js +6024 -0
- package/index.d.ts +126 -0
- package/index.js +190 -34
- package/interop.js +71 -27
- package/layout.js +5 -0
- package/module/array.js +71 -39
- package/module/collection.js +41 -5
- package/module/core.js +2 -4
- package/module/math.js +138 -2
- package/module/number.js +21 -0
- package/module/object.js +26 -0
- package/module/simd.js +37 -5
- package/module/string.js +41 -12
- package/module/typedarray.js +415 -26
- package/package.json +21 -6
- package/src/autoload.js +3 -0
- package/src/compile/analyze-scans.js +174 -11
- package/src/compile/analyze.js +38 -3
- package/src/compile/emit-assign.js +9 -6
- package/src/compile/emit.js +347 -36
- package/src/compile/index.js +307 -29
- package/src/compile/infer.js +36 -1
- package/src/compile/loop-divmod.js +155 -0
- package/src/compile/narrow.js +108 -11
- package/src/compile/peel-stencil.js +261 -0
- package/src/compile/plan/advise.js +55 -1
- package/src/compile/plan/index.js +4 -0
- package/src/compile/plan/inline.js +5 -2
- package/src/compile/plan/literals.js +220 -5
- package/src/compile/plan/scope.js +115 -39
- package/src/compile/program-facts.js +21 -2
- package/src/ctx.js +45 -7
- package/src/ir.js +55 -7
- package/src/kind-traits.js +27 -0
- package/src/kind.js +65 -3
- package/src/optimize/index.js +344 -45
- package/src/optimize/vectorize.js +2968 -182
- package/src/prepare/index.js +64 -7
- package/src/prepare/lift-iife.js +149 -0
- package/src/reps.js +3 -2
- package/src/static.js +9 -0
- package/src/type.js +10 -6
- package/src/wat/assemble.js +195 -13
- package/src/wat/optimize.js +363 -185
package/README.md
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
**jz** (_javascript zero_) is **minimal functional JS** that compiles to performant WASM.
|
|
6
6
|
|
|
7
|
-
|
|
8
7
|
```js
|
|
9
8
|
import jz from 'jz'
|
|
10
9
|
|
|
@@ -12,19 +11,23 @@ const { exports: { dist } } = jz`export let dist = (x, y) => (x*x + y*y) ** 0.5`
|
|
|
12
11
|
dist(3, 4) // 5
|
|
13
12
|
```
|
|
14
13
|
|
|
15
|
-
**[repl](https://dy.github.io/jz/repl/)** · **[examples](https://dy.github.io/jz/examples/)** · **[bench](https://dy.github.io/jz/bench/)**
|
|
14
|
+
**[repl](https://dy.github.io/jz/repl/)** · **[floatbeat](https://dy.github.io/jz/floatbeat/)** · **[examples](https://dy.github.io/jz/examples/)** · **[bench](https://dy.github.io/jz/bench/)**
|
|
16
15
|
|
|
17
16
|
|
|
18
17
|
## Why?
|
|
19
18
|
|
|
20
|
-
|
|
19
|
+
JZ distills **"the good parts"** ([Crockford](https://www.youtube.com/watch?v=_DKkVvOt6dk)) and **compiles JS ahead-of-time to WASM**: no runtime, no GC, no legacy, no spec creep, near-native perf with unlocked SIMD. **Valid JZ is valid JS** – run and test as JS, compile to WASM.
|
|
20
|
+
|
|
21
|
+
| Good for | Not for |
|
|
22
|
+
|------------------------------|---------------------------|
|
|
23
|
+
| DSP, audio, synthesis | UI, DOM, the frontend |
|
|
24
|
+
| Image, video, pixels | Servers, APIs, I/O |
|
|
25
|
+
| Simulation, physics, games | Async, promises, events |
|
|
26
|
+
| Parsers, codecs, compression | Dynamic, polymorphic, OOP |
|
|
27
|
+
| Scientific, numeric, ML | Security crypto, big-ints |
|
|
28
|
+
| Hashing, checksums, RNG | Glue, plumbing, orchestration |
|
|
21
29
|
|
|
22
|
-
|
|
23
|
-
|-----------------------------|----------------------------|
|
|
24
|
-
| Numeric / math compute | UI / frontend |
|
|
25
|
-
| DSP / audio / bytebeats | Backend / APIs |
|
|
26
|
-
| Parsing / transforms | Async / I/O-heavy logic |
|
|
27
|
-
| WASM utilities | JavaScript runtime |
|
|
30
|
+
Output `.wasm` is portable — run it in any host (browser, Node, Deno, edge, plugins), or take it native via [wasm2c](https://github.com/WebAssembly/wabt) (wasm → C → binary).
|
|
28
31
|
|
|
29
32
|
|
|
30
33
|
## Usage
|
|
@@ -32,7 +35,7 @@ jz distills **"the good parts"** ([Crockford](https://www.youtube.com/watch?v=_D
|
|
|
32
35
|
`npm install jz`
|
|
33
36
|
|
|
34
37
|
```js
|
|
35
|
-
import jz, { compile } from 'jz'
|
|
38
|
+
import jz, { compile, compileModule, instantiate } from 'jz'
|
|
36
39
|
|
|
37
40
|
// Compile + instantiate
|
|
38
41
|
const { exports: { add } } = jz('export let add = (a, b) => a + b')
|
|
@@ -41,6 +44,10 @@ add(2, 3) // 5
|
|
|
41
44
|
// Compile only — raw WASM binary
|
|
42
45
|
const wasm = compile('export let f = (x) => x * 2')
|
|
43
46
|
|
|
47
|
+
// Compile once → instantiate many (pays the AOT + validate cost once)
|
|
48
|
+
const mod = compileModule('export let f = (x) => x * 2')
|
|
49
|
+
instantiate(mod).exports.f(21) // 42 — repeat cheaply, no recompile
|
|
50
|
+
|
|
44
51
|
// Async startup
|
|
45
52
|
const asyncMod = await WebAssembly.compile(wasm)
|
|
46
53
|
const asyncInst = await WebAssembly.instantiate(asyncMod)
|
|
@@ -56,14 +63,20 @@ Options are passed as `jz(source, opts)` or `compile(source, opts)`. Common ones
|
|
|
56
63
|
|---|---|
|
|
57
64
|
| `modules: { specifier: source }` | Static ES imports to bundle. CLI import resolution does this from files automatically. |
|
|
58
65
|
| `imports: { mod: host }` | Host imports `import { fn } from "mod"`. |
|
|
59
|
-
| `memory` | Pass `memory: N` for owned memory with `N` initial pages, or `memory: jz.memory()` / `WebAssembly.Memory` to share across modules. |
|
|
66
|
+
| `memory` | Pass `memory: N` for owned memory with `N` initial pages, or `memory: jz.memory()` / `WebAssembly.Memory` to share across modules. `maxMemory: N` caps growth; `importMemory: true` imports `env.memory` instead of exporting own. |
|
|
60
67
|
| `host: 'js' \| 'wasi'` | Runtime-service lowering. Default `js`; `wasi` for standalone runtimes. |
|
|
61
|
-
| `optimize` | `false`/`0` off, `1`
|
|
68
|
+
| `optimize` | `false`/`0` off, `1` minimal, `true`/`2` default (all stable passes), `3`/`'speed'` trades size for speed, `'size'` for smallest wasm. (Object form for per-pass overrides is internal/unstable.) |
|
|
69
|
+
| `define` | Compile-time constants injected as top-level bindings, e.g. `{ DEBUG: false, PORT: 8080 }` (numbers, booleans, strings, null, or literal arrays/objects). |
|
|
62
70
|
| `strict: true` | Enforce the pure canonical subset: skip jzify lowering (so `var`/`function`/`class`/`==`/… are rejected, not accepted) **and** reject dynamic fallbacks (`obj[k]`, `for-in`, unknown receiver methods). Off by default — broader JS is lowered automatically. |
|
|
63
71
|
| `alloc: false` | Omit allocator exports (`_alloc`/`_clear`) for standalone modules that never marshal heap values. |
|
|
72
|
+
| `noSimd: true` | Disable auto-vectorization (no jz-emitted `v128`) for engines without the SIMD proposal. Explicit `f32x4`/`i32x4` intrinsics still compile. |
|
|
73
|
+
| `whyNotSimd: true` | Diagnostic (CLI `--why-not-simd`): emit a `simd-why-not` warning per loop the auto-vectorizer declined, naming the first blocking op — finds loops one op away from SIMD. Noisy; off by default. Surfaced via the `warnings` sink. |
|
|
74
|
+
| `experimentalStencil: true` | Opt-in (CLI `--experimental-stencil`): vectorize neighbour-load stencils — `b[i] = f(a[i-1], a[i], a[i+1])` and 2-D 5-point sweeps — to f64x2. Bit-exact vs scalar (a lane-parallel map reorders nothing within a lane). Unstable / off by default until proven across the corpus. |
|
|
75
|
+
| `experimentalOuterStrip: true` | Opt-in (CLI `--experimental-outer-strip`): strip-mine a pixel loop whose per-pixel value is an inner reduction (e.g. metaballs' field sum over blobs) into f64x2 — two adjacent pixels per step. Bit-exact vs scalar (each lane accumulates in scalar order). Unstable / off by default. |
|
|
64
76
|
| `randomSeed` | `Math.random` seeding — default draws from host entropy (non-reproducible); a number fixes it for a reproducible sequence, `true` forces entropy explicitly. |
|
|
65
77
|
| `wat: true` | `compile()` returns WAT text instead of WASM binary. |
|
|
66
|
-
| `
|
|
78
|
+
| `names: true` | Emit a WASM `name` section (function symbols) for profilers/debuggers. |
|
|
79
|
+
| `profile` | Mutable sink for compile-stage timings (`entries`/`totals` per phase). |
|
|
67
80
|
</details>
|
|
68
81
|
|
|
69
82
|
## CLI
|
|
@@ -74,7 +87,7 @@ Options are passed as `jz(source, opts)` or `compile(source, opts)`. Common ones
|
|
|
74
87
|
jz program.js # → program.wasm
|
|
75
88
|
jz program.js --wat # → program.wat
|
|
76
89
|
jz program.js -o out.wasm # custom output (- for stdout)
|
|
77
|
-
jz program.js -O3 # optimization: -O0 off, -O1
|
|
90
|
+
jz program.js -O3 # optimization: -O0 off, -O1 minimal, -O2 default, -O3 speed (-Os for size)
|
|
78
91
|
jz program.js --host wasi # standalone WASI output
|
|
79
92
|
jz --strict program.js # pure canonical subset (also implied by .jz extension)
|
|
80
93
|
jz -e "1 + 2" # eval → 3
|
|
@@ -84,7 +97,7 @@ jz -e "1 + 2" # eval → 3
|
|
|
84
97
|
<summary><code>jz --help</code></summary>
|
|
85
98
|
|
|
86
99
|
```
|
|
87
|
-
jz
|
|
100
|
+
jz - min JS → WASM compiler
|
|
88
101
|
|
|
89
102
|
Usage:
|
|
90
103
|
jz <file.js> Compile JS to WASM (full JS subset; .jz = strict)
|
|
@@ -98,8 +111,10 @@ Examples:
|
|
|
98
111
|
jz program.js --wat # → program.wat
|
|
99
112
|
jz program.js -o out.wasm # custom output name
|
|
100
113
|
jz program.js -o - # write to stdout
|
|
101
|
-
jz program.js -O3 #
|
|
114
|
+
jz program.js -O3 # optimize for speed
|
|
102
115
|
jz program.js -Os # optimize for size
|
|
116
|
+
jz program.js -D DEBUG=false # inject a compile-time constant
|
|
117
|
+
jz program.js --memory 64 # 64 initial pages (4 MB)
|
|
103
118
|
jz program.js --host wasi # emit WASI Preview 1 imports
|
|
104
119
|
jz --strict program.js # strict mode
|
|
105
120
|
jz --jzify lib.js # → lib.jz
|
|
@@ -107,11 +122,22 @@ Examples:
|
|
|
107
122
|
|
|
108
123
|
Options:
|
|
109
124
|
--output, -o <file> Output file (.wat, .wasm, or - for stdout)
|
|
110
|
-
-O<n>, --optimize <n> Optimization level: 0 off, 1
|
|
111
|
-
3
|
|
125
|
+
-O<n>, --optimize <n> Optimization level: 0 off, 1 minimal, 2 default (all
|
|
126
|
+
stable passes), 3 speed. -Os optimizes for size.
|
|
127
|
+
--define, -D <K=V> Inject a compile-time constant (VALUE parsed as JSON,
|
|
128
|
+
else string). Repeatable.
|
|
112
129
|
--host <js|wasi> Runtime-service lowering (default js)
|
|
130
|
+
--memory <pages> Initial memory size in 64 KiB pages
|
|
131
|
+
--max-memory <pages> Cap memory growth at this many pages (default unbounded)
|
|
132
|
+
--import-memory Import env.memory instead of exporting own memory
|
|
113
133
|
--no-alloc Omit _alloc/_clear allocator exports (standalone wasm)
|
|
134
|
+
--no-simd Disable auto-vectorization (no v128) for non-SIMD engines
|
|
135
|
+
--why-not-simd Report, per loop, why the auto-vectorizer declined it
|
|
136
|
+
--experimental-stencil Vectorize neighbour-load stencils (a[i±1]); opt-in
|
|
137
|
+
--experimental-outer-strip Strip-mine pixel loops over an inner reduction to f64x2; opt-in
|
|
138
|
+
--no-tail-call Use ordinary call frames instead of return_call
|
|
114
139
|
--names Emit wasm name section for profilers/debuggers
|
|
140
|
+
--stats Print compile-phase timings to stderr
|
|
115
141
|
--strict Pure canonical subset: reject full-JS syntax + dynamic fallbacks
|
|
116
142
|
--jzify Transform JS to jz source (no compilation)
|
|
117
143
|
--eval, -e Evaluate expression or file
|
|
@@ -345,7 +371,7 @@ No runtime, no GC — a module is your code plus a small bump allocator. The geo
|
|
|
345
371
|
|
|
346
372
|
- **`optimize: 'size'`** — keeps every size pass, drops loop unrolling and SIMD.
|
|
347
373
|
- **`alloc: false`** — omit the allocator for pure-numeric modules that never marshal heap values.
|
|
348
|
-
- **`host: 'wasi'`** — no JS-host import shims (the debug `name` section is already off unless you set `
|
|
374
|
+
- **`host: 'wasi'`** — no JS-host import shims (the debug `name` section is already off unless you set `names: true`).
|
|
349
375
|
|
|
350
376
|
Hand-written WAT is still ~3–8× smaller on tight kernels — jz carries generic allocator and stdlib helpers a specialist omits; closing that gap is ongoing. Size budgets are gated in CI alongside speed ([full table](bench/README.md)).
|
|
351
377
|
|
|
@@ -363,7 +389,7 @@ Ordinary JS is already fast — jz infers the right machine type for your number
|
|
|
363
389
|
- **SIMD-128** — independent iterations (`a[i] = a[i]*2 + b[i]`) run several lanes at once: lane-pure maps, reductions (sum/product/min·max), conditional maps (`bitselect`), byte scans (`memchr` via `i8x16`). Loops that look back (`a[i-1]`) or carry a running total stay sequential.
|
|
364
390
|
- **Smaller encoding** — tree-shaking, copy-propagation + dead-store elimination, local/string-pool reordering for 1-byte indices, pointer-call specialization, constant pooling; JS strings you only read aren't copied.
|
|
365
391
|
|
|
366
|
-
Codegen also adapts to the target: `host: 'js'` lowers `console`/timers to tiny `env.*` imports, a constant `JSON.parse` folds to a literal, JS strings stay zero-copy. Levels `0`–`3`
|
|
392
|
+
Codegen also adapts to the target: `host: 'js'` lowers `console`/timers to tiny `env.*` imports, a constant `JSON.parse` folds to a literal, JS strings stay zero-copy. Levels `0`–`3` (default `2`), or the named presets `'speed'` (= `3`, trades size for inlined constants and larger buffers) and `'size'` (drops unrolling and SIMD for the smallest wasm).
|
|
367
393
|
|
|
368
394
|
</details>
|
|
369
395
|
|
|
@@ -371,9 +397,9 @@ Codegen also adapts to the target: `host: 'js'` lowers `console`/timers to tiny
|
|
|
371
397
|
<summary><strong>How do I inspect or debug the output?</strong></summary>
|
|
372
398
|
|
|
373
399
|
- **Semantics** — valid jz is valid JS: run the same source under Node and diff results (mind the [documented divergences](#faq)); `console.log` works inside compiled modules too.
|
|
374
|
-
- **Codegen** — `jz program.js --wat` (API: `compile(src, { wat: true })`) shows the emitted WAT: grep `v128` to confirm a loop vectorized, `__dyn_get`/`__ext_call` to spot dynamic fallbacks inference couldn't narrow.
|
|
400
|
+
- **Codegen** — `jz program.js --wat` (API: `compile(src, { wat: true })`) shows the emitted WAT: grep `v128` to confirm a loop vectorized, `__dyn_get`/`__ext_call` to spot dynamic fallbacks inference couldn't narrow. `--why-not-simd` (API: `whyNotSimd: true`) goes further — for each loop the auto-vectorizer declined it reports the first blocking op (e.g. `i32.rem_s: no lane-pure SIMD mapping`), so you don't have to grep the WAT to find what's one op away.
|
|
375
401
|
- **Dynamic fallbacks** — compile with `strict: true` to turn every fallback (`obj[k]`, `for-in`, unknown receiver method) into a compile error pointing at the site.
|
|
376
|
-
- **Profiling** — `--names` (API: `
|
|
402
|
+
- **Profiling** — `--names` (API: `names: true`) emits a wasm `name` section so DevTools profilers and disassemblers show real function names; `--stats` (API: the `profile` sink) collects per-stage compile timings.
|
|
377
403
|
- **Slow kernel checklist** — a stray float literal pins a counter to f64; a plain `[]` where a typed array would do; a loop-carried dependency (`a[i-1]`, running sum) blocks SIMD. The signals in *Why no type annotations?* below are the levers.
|
|
378
404
|
|
|
379
405
|
</details>
|
|
@@ -462,49 +488,40 @@ The full native pipeline (jz → `wasm-opt -O3` → `wasm2c` → `clang -O3 -flt
|
|
|
462
488
|
|
|
463
489
|
|
|
464
490
|
|
|
465
|
-
|
|
466
|
-
|
|
467
491
|
## Performance
|
|
468
492
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
<img src="bench/bench.svg?v=2" alt="jz vs alternatives — geomean speed across the bench corpus" width="720">
|
|
493
|
+
<img src="bench/bench.svg?v=5" alt="jz vs alternatives — geometric-mean speed across the bench corpus, every rival compiled to WebAssembly and run in V8 (apples-to-apples); native C is the lone reference, jz = 1.00× baseline" width="720">
|
|
472
494
|
|
|
473
|
-
<sub>Local snapshot (M4 Max, darwin/arm64). Bun/Zig/Rust/Go/NumPy rows are hand-run reference points.</sub>
|
|
474
495
|
|
|
496
|
+
See [bench →](https://dy.github.io/jz/bench/)
|
|
475
497
|
|
|
476
498
|
## Examples
|
|
477
499
|
|
|
478
500
|
<table>
|
|
479
501
|
<tr>
|
|
480
|
-
<td width="33%"><a href="https://dy.github.io/jz/examples/
|
|
481
|
-
<td width="33%"><a href="https://dy.github.io/jz/examples/
|
|
482
|
-
<td width="33%"><a href="https://dy.github.io/jz/examples/
|
|
502
|
+
<td width="33%"><a href="https://dy.github.io/jz/examples/chladni/"><img src="examples/thumbs/chladni.webp" width="100%" alt="Chladni plate"></a><br><b>chladni</b> — frequency sweeps the nodal figure.</td>
|
|
503
|
+
<td width="33%"><a href="https://dy.github.io/jz/examples/julia/"><img src="examples/thumbs/julia.webp" width="100%" alt="Julia set"></a><br><b>julia</b> — escape-time Julia set; drag the constant to morph it.</td>
|
|
504
|
+
<td width="33%"><a href="https://dy.github.io/jz/examples/attractors/"><img src="examples/thumbs/attractors.webp" width="100%" alt="Strange attractor"></a><br><b>attractors</b> — de Jong map, luminous curves.</td>
|
|
483
505
|
</tr>
|
|
484
506
|
<tr>
|
|
485
|
-
<td><a href="https://dy.github.io/jz/examples/interference/"><img src="examples/thumbs/interference.webp" width="100%" alt="Wave interference"></a><br><b>interference</b> — two-source wave field, recomputed every frame.</td>
|
|
486
|
-
<td><a href="https://dy.github.io/jz/examples/plasma/"><img src="examples/thumbs/plasma.webp" width="100%" alt="Plasma"></a><br><b>plasma</b> — FBM domain-warp; the classic flowing shader plasma.</td>
|
|
487
|
-
<td><a href="https://dy.github.io/jz/examples/chladni/"><img src="examples/thumbs/chladni.webp" width="100%" alt="Chladni plate"></a><br><b>chladni</b> — Camerata-style plate; frequency sweeps the nodal figure.</td>
|
|
488
|
-
</tr>
|
|
489
|
-
<tr>
|
|
490
|
-
<td><a href="https://dy.github.io/jz/examples/mandelbrot/"><img src="examples/thumbs/mandelbrot.webp" width="100%" alt="Mandelbrot set"></a><br><b>mandelbrot</b> — escape-time fractal with smooth coloring.</td>
|
|
491
|
-
<td><a href="https://dy.github.io/jz/examples/attractors/"><img src="examples/thumbs/attractors.webp" width="100%" alt="Strange attractor"></a><br><b>attractors</b> — de Jong map, millions of iters → luminous curves.</td>
|
|
492
507
|
<td><a href="https://dy.github.io/jz/examples/raymarcher/"><img src="examples/thumbs/raymarcher.webp" width="100%" alt="SDF raymarcher"></a><br><b>raymarcher</b> — an SDF sphere field; Shadertoy on the CPU.</td>
|
|
508
|
+
<td><a href="https://dy.github.io/jz/examples/nbody/"><img src="examples/thumbs/nbody.webp" width="100%" alt="N-body gravity"></a><br><b>nbody</b> — three-body gravity; fading trails trace the orbits.</td>
|
|
509
|
+
<td><a href="https://dy.github.io/jz/examples/game-of-life/"><img src="examples/thumbs/game-of-life.webp" width="100%" alt="Game of Life"></a><br><b>game-of-life</b> — Conway's Life straight into shared pixel memory.</td>
|
|
493
510
|
</tr>
|
|
494
511
|
<tr>
|
|
495
|
-
<td><a href="https://dy.github.io/jz/examples/
|
|
496
|
-
<td><a href="https://dy.github.io/jz/examples/
|
|
497
|
-
<td><a href="https://dy.github.io/jz/examples/
|
|
512
|
+
<td><a href="https://dy.github.io/jz/examples/plasma/"><img src="examples/thumbs/plasma.webp" width="100%" alt="Plasma"></a><br><b>plasma</b> — FBM domain-warp; the classic flowing shader plasma.</td>
|
|
513
|
+
<td><a href="https://dy.github.io/jz/examples/cloth/"><img src="examples/thumbs/cloth.webp" width="100%" alt="Cloth simulation"></a><br><b>cloth</b> — Verlet mass-spring sheet; drag it, watch it settle.</td>
|
|
514
|
+
<td><a href="https://dy.github.io/jz/examples/erosion/"><img src="examples/thumbs/erosion.webp" width="100%" alt="Terrain erosion"></a><br><b>erosion</b> — hydraulic droplets carve a fractal terrain.</td>
|
|
498
515
|
</tr>
|
|
499
516
|
</table>
|
|
500
517
|
|
|
501
|
-
[
|
|
518
|
+
See [all examples →](https://dy.github.io/jz/examples/)
|
|
502
519
|
|
|
503
520
|
|
|
504
521
|
|
|
505
522
|
## Alternatives
|
|
506
523
|
|
|
507
|
-
|
|
524
|
+
Small & fast JS subset → full JS spec & bundled engine:
|
|
508
525
|
|
|
509
526
|
* [AssemblyScript](https://github.com/AssemblyScript/assemblyscript) — TS-like dialect → WASM; small, fast output, but needs type annotations (not JS).
|
|
510
527
|
* [awasm-compiler](https://github.com/paulmillr/awasm-compiler) — reproducible WASM assembled through a typed *builder API*.
|
|
@@ -521,4 +538,10 @@ From small, fast JS subset to full JS spec, bundled engine:
|
|
|
521
538
|
* [**watr**](https://www.npmjs.com/package/watr) — WAT to WASM compiler. Binary encoding, validation, and peephole optimization. jz emits WAT text, watr turns it into valid `.wasm`.
|
|
522
539
|
|
|
523
540
|
|
|
541
|
+
## Contributing
|
|
542
|
+
|
|
543
|
+
Setup, code layout, and the bench/perf invariants are in [CONTRIBUTING.md](docs/CONTRIBUTING.md);
|
|
544
|
+
the architecture & design rationale (NaN-boxing, type inference, native pipeline) in [DESIGN.md](docs/DESIGN.md).
|
|
545
|
+
|
|
546
|
+
|
|
524
547
|
<p align=center>MIT • <a href="https://github.com/krishnized/license/">ॐ</a></p>
|
package/bench/README.md
CHANGED
|
@@ -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,10 +50,21 @@ 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 |
|
|
54
58
|
| [`watr`](watr/watr.js) | watr's WAT-to-wasm compiler on a small WAT corpus; compares jz-compiled compiler code with raw V8 |
|
|
55
59
|
| [`jessie`](jessie/jessie.js) | the subscript/jessie JS parser over a realistic source corpus; branch-, allocation- and recursion-heavy front-end work |
|
|
56
60
|
| [`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 |
|
|
57
61
|
|
|
62
|
+
The `watr`, `jessie`, and `jz` rows are self-referential — jz (or its deps)
|
|
63
|
+
compiling code. They stay runnable (`--cases=watr,jessie,jz`) and gated in
|
|
64
|
+
`test/bench.js`, but are hidden from the bench page and the headline geomean SVG:
|
|
65
|
+
they answer a different question (compiler throughput on itself) from the
|
|
66
|
+
cross-language kernel comparison the page is about.
|
|
67
|
+
|
|
58
68
|
Native rows for `json` are fixed-source references, not semantic equivalents
|
|
59
69
|
of JavaScript `JSON.parse`: C/Rust/Zig hand-parse the known schema from a
|
|
60
70
|
compile-time string, and Zig may constant-fold the whole parse+walk under
|
|
@@ -64,9 +74,16 @@ compile-time folded, while the compiler can still specialize the stable literal
|
|
|
64
74
|
shape. External unknown-shape JSON still uses the generic runtime parser.
|
|
65
75
|
|
|
66
76
|
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
|
-
|
|
77
|
+
where a vectorized array implementation is a meaningful Python convention.
|
|
78
|
+
|
|
79
|
+
Pure interpreters (CPython, QuickJS, Hermes, Javy's embedded-QuickJS) are not
|
|
80
|
+
benchmarked: this suite measures compiled-code quality, and an interpreter row
|
|
81
|
+
inflates jz's lead for free without answering a question a user actually has.
|
|
82
|
+
The headline field is wasm-vs-wasm, apples-to-apples: jz against the other
|
|
83
|
+
languages compiled to the same target — WebAssembly run in V8 — i.e. Rust, Go,
|
|
84
|
+
and C (`wasm32-wasi`), AssemblyScript, and Porffor, plus the JS JITs it replaces
|
|
85
|
+
(V8/Bun/Deno/SpiderMonkey/GraalJS). Native C/Rust/Zig/Go and NumPy's vectorized
|
|
86
|
+
C are kept only as a speed-of-light reference, never presented as the wasm peer.
|
|
70
87
|
|
|
71
88
|
### Parity classes
|
|
72
89
|
|
|
@@ -84,30 +101,30 @@ correctly-rounded; cascade is the same algorithm.
|
|
|
84
101
|
| --- | --- |
|
|
85
102
|
| `nat` | clang `-O3` native C baseline, when a matching C workload exists |
|
|
86
103
|
| `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 |
|
|
104
|
+
| `rust` | Rust `rustc -C opt-level=3 -C target-cpu=native` — native baseline; the headline rival is `rust-wasm` |
|
|
105
|
+
| `go` | Go native compiler — native baseline; the headline rival is `go-wasm` |
|
|
106
|
+
| `zig` | Zig `build-exe -O ReleaseFast` — native baseline |
|
|
91
107
|
| `numpy` | vectorized NumPy, when a matching `.npy.py` exists |
|
|
92
108
|
| `v8` | raw JavaScript on Node/V8 |
|
|
93
109
|
| `deno` | raw JavaScript on Deno/V8 |
|
|
94
110
|
| `bun` | raw JavaScript on Bun/JavaScriptCore |
|
|
95
111
|
| `spidermonkey` | raw JavaScript on SpiderMonkey shell (`spidermonkey`, `sm`, `js128`, `js115`, `js102`, or `js`) |
|
|
96
|
-
| `hermes` | raw JavaScript on Hermes |
|
|
97
112
|
| `shermes` | Static Hermes — JS AOT-compiled to a native binary (`shermes -O`), when installed |
|
|
98
113
|
| `graaljs` | raw JavaScript on GraalJS |
|
|
99
114
|
| `jz` | jz output with host imports for timing/logging (measures wasm size without WASI console/perf bloat) |
|
|
100
115
|
| `as` | AssemblyScript `asc -O3 --runtime stub`, when a matching `.as.ts` exists |
|
|
116
|
+
| `rust-wasm` | Rust → `wasm32-wasip1` (`rustc --target wasm32-wasip1 -C opt-level=3`), run in node's V8 — the apples-to-apples Rust rival |
|
|
117
|
+
| `go-wasm` | Go → `wasm32-wasip1` (`GOOS=wasip1 GOARCH=wasm go build`), run in node's V8 |
|
|
118
|
+
| `c-wasm` | C → `wasm32-wasi` (`zig cc -target wasm32-wasi -O3`, no emcc/wasi-sdk needed), run in node's V8 |
|
|
101
119
|
| `jz-wasmtime` | jz output on wasmtime |
|
|
102
120
|
| `jz-w2c` | jz wasm translated by wabt `wasm2c`, then clang `-O3` |
|
|
103
121
|
| `wat` | hand-written WAT baseline when a case provides `run-wat.mjs` |
|
|
104
|
-
| `qjs` | QuickJS when installed |
|
|
105
122
|
| `porf` | Porffor (`porf run`) when installed |
|
|
106
123
|
| `jawsm` | jawsm when installed |
|
|
107
124
|
|
|
108
125
|
The `size` column reports the artifact size each target measures: the
|
|
109
126
|
compiled native binary for `nat`/`rust`/`go`/`zig`, the produced
|
|
110
|
-
`.wasm` for `jz`/`as`/hand-WAT/jawsm/`jz-w2c` (the C-translated
|
|
127
|
+
`.wasm` for `jz`/`as`/`rust-wasm`/`go-wasm`/`c-wasm`/hand-WAT/jawsm/`jz-w2c` (the C-translated
|
|
111
128
|
executable), or the source file for raw-JS interpreters where there is no
|
|
112
129
|
compile step. For source files with imports, raw-JS size is only the entry file;
|
|
113
130
|
jz size is the bundled wasm artifact.
|
|
@@ -130,11 +147,10 @@ This keeps the target measuring the best current jz artifact for that workload.
|
|
|
130
147
|
BUN_BIN=/path/to/bun \
|
|
131
148
|
DENO_BIN=/path/to/deno \
|
|
132
149
|
SPIDERMONKEY_BIN=/path/to/js \
|
|
133
|
-
HERMES_BIN=/path/to/hermes \
|
|
134
150
|
SHERMES_BIN=/path/to/shermes \
|
|
135
151
|
GRAALJS_BIN=/path/to/graaljs \
|
|
136
152
|
PORF_BIN=/path/to/porf \
|
|
137
|
-
node bench/bench.mjs --targets=bun,deno,spidermonkey,
|
|
153
|
+
node bench/bench.mjs --targets=bun,deno,spidermonkey,shermes,graaljs,porf
|
|
138
154
|
```
|
|
139
155
|
|
|
140
156
|
## Reading the numbers (darwin/arm64, M-class)
|
|
@@ -148,13 +164,13 @@ available from earlier runs.
|
|
|
148
164
|
|
|
149
165
|
| target | median | ×v8 | size | parity |
|
|
150
166
|
| --- | ---: | ---: | ---: | --- |
|
|
151
|
-
| **jz → V8 wasm** | **
|
|
152
|
-
| AssemblyScript (asc -O3 --runtime stub) |
|
|
153
|
-
| V8 (node) raw JS |
|
|
154
|
-
| native C (clang -O3) |
|
|
167
|
+
| **jz → V8 wasm** | **4.64 ms** | **1.94×** | **3.4 kB** | **ok** |
|
|
168
|
+
| AssemblyScript (asc -O3 --runtime stub) | 6.51 ms | 1.38× | 1.9 kB | ok |
|
|
169
|
+
| V8 (node) raw JS | 8.98 ms | 1.00× | 3.2 kB | ok |
|
|
170
|
+
| native C (clang -O3) | 3.86 ms | 2.32× | 32.7 kB | ok |
|
|
155
171
|
| hand-WAT → V8 wasm | 6.49 ms | 1.90× | 767 B | ok |
|
|
156
172
|
|
|
157
|
-
jz beats V8 raw JS by 1
|
|
173
|
+
jz beats V8 raw JS by 2.1× and AS by 1.4×. The typed-array scalarization,
|
|
158
174
|
offset-fusion, and base-hoisting pipeline delivers dense-f64 loop codegen
|
|
159
175
|
that matches the hand-WAT floor.
|
|
160
176
|
|
|
@@ -162,13 +178,13 @@ that matches the hand-WAT floor.
|
|
|
162
178
|
|
|
163
179
|
| target | median | ×v8 | size | parity |
|
|
164
180
|
| --- | ---: | ---: | ---: | --- |
|
|
165
|
-
| **jz → V8 wasm** | **
|
|
166
|
-
| AssemblyScript (asc -O3 --runtime stub) |
|
|
167
|
-
| V8 (node) raw JS |
|
|
168
|
-
| native C (clang -O3) |
|
|
181
|
+
| **jz → V8 wasm** | **1.49 ms** | **5.75×** | **3.1 kB** | **ok** |
|
|
182
|
+
| AssemblyScript (asc -O3 --runtime stub) | 6.71 ms | 1.28× | 1.6 kB | ok |
|
|
183
|
+
| V8 (node) raw JS | 8.57 ms | 1.00× | 1.2 kB | ok |
|
|
184
|
+
| native C (clang -O3) | 1.97 ms | 4.35× | 32.8 kB | ok |
|
|
169
185
|
| hand-WAT → V8 wasm | 8.12 ms | 1.47× | 414 B | ok |
|
|
170
186
|
|
|
171
|
-
jz is
|
|
187
|
+
jz is 5.9× faster than V8 raw JS and 4.6× faster than AS. The scalarized
|
|
172
188
|
SIMD hot path (unrolled 4×4 multiply) is the win; V8's JIT doesn't vectorize
|
|
173
189
|
this from JS source.
|
|
174
190
|
|
|
@@ -176,11 +192,11 @@ this from JS source.
|
|
|
176
192
|
|
|
177
193
|
| target | median | ×v8 | size | parity |
|
|
178
194
|
| --- | ---: | ---: | ---: | --- |
|
|
179
|
-
| **jz → V8 wasm** | **0.
|
|
180
|
-
| AssemblyScript (asc -O3 --runtime stub) |
|
|
181
|
-
| V8 (node) raw JS |
|
|
195
|
+
| **jz → V8 wasm** | **0.13 ms** | **12.67×** | **1.4 kB** | **ok** |
|
|
196
|
+
| AssemblyScript (asc -O3 --runtime stub) | 0.81 ms | 2.07× | 1.3 kB | ok |
|
|
197
|
+
| V8 (node) raw JS | 1.67 ms | 1.00× | 1014 B | ok |
|
|
182
198
|
|
|
183
|
-
jz is
|
|
199
|
+
jz is 12.9× faster than V8 raw JS and 5.9× faster than AS. The bimorphic
|
|
184
200
|
`sum` (called with both `Float64Array` and `Int32Array`) stays on typed
|
|
185
201
|
paths without falling back to generic dispatch.
|
|
186
202
|
|
|
@@ -188,13 +204,13 @@ paths without falling back to generic dispatch.
|
|
|
188
204
|
|
|
189
205
|
| target | median | ×v8 | size | parity |
|
|
190
206
|
| --- | ---: | ---: | ---: | --- |
|
|
191
|
-
| **jz → V8 wasm** | **1.
|
|
192
|
-
| V8 (node) raw JS |
|
|
193
|
-
| AssemblyScript (asc -O3 --runtime stub) |
|
|
194
|
-
| native C (clang -O3) |
|
|
195
|
-
| hand-WAT → V8 wasm |
|
|
207
|
+
| **jz → V8 wasm** | **1.01 ms** | **3.84×** | **1.2 kB** | **ok** |
|
|
208
|
+
| V8 (node) raw JS | 3.87 ms | 1.00× | 1005 B | ok |
|
|
209
|
+
| AssemblyScript (asc -O3 --runtime stub) | 9.15 ms | 0.42× | 1.5 kB | ok |
|
|
210
|
+
| native C (clang -O3) | 0.95 ms | 4.08× | 32.9 kB | ok |
|
|
211
|
+
| hand-WAT → V8 wasm | 3.59 ms | 1.11× | 355 B | ok |
|
|
196
212
|
|
|
197
|
-
jz is
|
|
213
|
+
jz is 4.0× faster than V8 raw JS and 8.8× faster than AS. The i32 hot path
|
|
198
214
|
(`Math.imul`, `|0`, `>>>0`) now lowers to raw `i32` ops without NaN-box
|
|
199
215
|
overhead on every operation.
|
|
200
216
|
|
|
@@ -202,24 +218,22 @@ overhead on every operation.
|
|
|
202
218
|
|
|
203
219
|
| target | median | ×v8 | size | parity |
|
|
204
220
|
| --- | ---: | ---: | ---: | --- |
|
|
205
|
-
|
|
|
206
|
-
|
|
|
207
|
-
| V8 (node) raw JS | 0.
|
|
221
|
+
| **jz → V8 wasm** | **0.06 ms** | **2.26×** | **2.2 kB** | **ok** |
|
|
222
|
+
| AssemblyScript (asc -O3 --runtime stub) | 0.06 ms | 2.15× | 1.6 kB | ok |
|
|
223
|
+
| V8 (node) raw JS | 0.13 ms | 1.00× | 2.0 kB | ok |
|
|
208
224
|
|
|
209
|
-
jz is 2.
|
|
210
|
-
`
|
|
211
|
-
jz still boxes the string pointer. The gap is narrow (0.02 ms) and both
|
|
212
|
-
are well ahead of V8.
|
|
225
|
+
jz is 2.4× faster than V8 raw JS and now edges out AS by ~1.2× on this
|
|
226
|
+
`charCodeAt`-heavy scan. Both are well ahead of V8.
|
|
213
227
|
|
|
214
228
|
### callback — `Array.map` closure + i32 fold
|
|
215
229
|
|
|
216
230
|
| target | median | ×v8 | size | parity |
|
|
217
231
|
| --- | ---: | ---: | ---: | --- |
|
|
218
|
-
| **jz → V8 wasm** | **0.
|
|
219
|
-
|
|
|
220
|
-
|
|
|
232
|
+
| **jz → V8 wasm** | **0.26 ms** | **2.37×** | **1.4 kB** | **ok** |
|
|
233
|
+
| V8 (node) raw JS | 0.62 ms | 1.00× | 1.3 kB | ok |
|
|
234
|
+
| AssemblyScript (asc -O3 --runtime stub) | 0.80 ms | 0.78× | 1.9 kB | ok |
|
|
221
235
|
|
|
222
|
-
jz is
|
|
236
|
+
jz is 2.3× faster than V8 raw JS and 2.9× faster than AS. Closure +
|
|
223
237
|
`Array.map` lowers to a preallocated typed loop with no per-iteration alloc.
|
|
224
238
|
V8's JIT does not inline the closure across the `map` boundary.
|
|
225
239
|
|
|
@@ -227,11 +241,11 @@ V8's JIT does not inline the closure across the `map` boundary.
|
|
|
227
241
|
|
|
228
242
|
| target | median | ×v8 | size | parity |
|
|
229
243
|
| --- | ---: | ---: | ---: | --- |
|
|
230
|
-
| **jz → V8 wasm** | **
|
|
231
|
-
| V8 (node) raw JS | 1.
|
|
232
|
-
| AssemblyScript (asc -O3 --runtime stub) | 1.
|
|
244
|
+
| **jz → V8 wasm** | **0.69 ms** | **1.93×** | **1.8 kB** | **ok** |
|
|
245
|
+
| V8 (node) raw JS | 1.33 ms | 1.00× | 1.1 kB | ok |
|
|
246
|
+
| AssemblyScript (asc -O3 --runtime stub) | 1.40 ms | 0.95× | 2.1 kB | ok |
|
|
233
247
|
|
|
234
|
-
jz is 1.
|
|
248
|
+
jz is 1.9× faster than V8 raw JS and 2.0× faster than AS. Schema-slot
|
|
235
249
|
reads are direct field offsets; the gap is small because the workload is
|
|
236
250
|
memory-bound.
|
|
237
251
|
|
|
@@ -239,9 +253,9 @@ memory-bound.
|
|
|
239
253
|
|
|
240
254
|
| target | median | ×v8 | size | parity |
|
|
241
255
|
| --- | ---: | ---: | ---: | --- |
|
|
242
|
-
| AssemblyScript (asc -O3 --runtime stub) |
|
|
243
|
-
| **jz → V8 wasm** | **
|
|
244
|
-
| V8 (node) raw JS |
|
|
256
|
+
| AssemblyScript (asc -O3 --runtime stub) | 8.90 ms | 1.11× | 1.3 kB | ok |
|
|
257
|
+
| **jz → V8 wasm** | **8.45 ms** | **1.17×** | **1.4 kB** | **ok** |
|
|
258
|
+
| V8 (node) raw JS | 9.90 ms | 1.00× | 1.8 kB | ok |
|
|
245
259
|
|
|
246
260
|
jz is 1.1× faster than V8 raw JS and ties AS. The dense f64 hot loop with
|
|
247
261
|
conditional break compacts to 1.0 kB — the smallest wasm in the suite.
|
|
@@ -250,40 +264,65 @@ conditional break compacts to 1.0 kB — the smallest wasm in the suite.
|
|
|
250
264
|
|
|
251
265
|
| target | median | ×v8 | size | parity |
|
|
252
266
|
| --- | ---: | ---: | ---: | --- |
|
|
253
|
-
| **jz → V8 wasm** | **0.
|
|
254
|
-
| V8 (node) raw JS | 0.
|
|
267
|
+
| **jz → V8 wasm** | **0.21 ms** | **1.28×** | **9.9 kB** | **ok** |
|
|
268
|
+
| V8 (node) raw JS | 0.27 ms | 1.00× | 1.2 kB | ok |
|
|
255
269
|
|
|
256
|
-
jz is 1.
|
|
270
|
+
jz is 1.3× faster than V8 raw JS. The runtime parser is specialized to the
|
|
257
271
|
inferred JSON shape; AS is skipped because it cannot parse JSON at runtime.
|
|
258
272
|
|
|
259
273
|
### sort — in-place heapsort over typed array
|
|
260
274
|
|
|
261
275
|
| target | median | ×v8 | size | parity |
|
|
262
276
|
| --- | ---: | ---: | ---: | --- |
|
|
263
|
-
| **jz → V8 wasm** | **
|
|
264
|
-
| AssemblyScript (asc -O3 --runtime stub) |
|
|
265
|
-
| V8 (node) raw JS |
|
|
277
|
+
| **jz → V8 wasm** | **4.65 ms** | **1.67×** | **1.8 kB** | **ok** |
|
|
278
|
+
| AssemblyScript (asc -O3 --runtime stub) | 7.96 ms | 0.98× | 1.9 kB | ok |
|
|
279
|
+
| V8 (node) raw JS | 7.79 ms | 1.00× | 1.6 kB | ok |
|
|
266
280
|
|
|
267
|
-
jz is 1.
|
|
281
|
+
jz is 1.6× faster than V8 raw JS and 1.4× faster than AS. Call-heavy
|
|
268
282
|
nested loops with typed-array index propagation stay on the i32 path.
|
|
269
283
|
|
|
270
284
|
### crc32 — table-driven CRC-32 over byte buffer
|
|
271
285
|
|
|
272
286
|
| target | median | ×v8 | size | parity |
|
|
273
287
|
| --- | ---: | ---: | ---: | --- |
|
|
274
|
-
| **jz → V8 wasm** | **
|
|
275
|
-
| AssemblyScript (asc -O3 --runtime stub) |
|
|
276
|
-
| V8 (node) raw JS |
|
|
288
|
+
| **jz → V8 wasm** | **8.74 ms** | **1.11×** | **1.5 kB** | **ok** |
|
|
289
|
+
| AssemblyScript (asc -O3 --runtime stub) | 8.76 ms | 1.11× | 1.4 kB | ok |
|
|
290
|
+
| V8 (node) raw JS | 9.71 ms | 1.00× | 1.8 kB | ok |
|
|
277
291
|
|
|
278
|
-
jz is 1.
|
|
292
|
+
jz is 1.2× faster than V8 raw JS and ties AS. Integer narrowing and
|
|
279
293
|
typed-array parameter propagation keep the LUT lookup on raw i32.
|
|
280
294
|
|
|
295
|
+
### Audio + image showcase (cross-language, bit-exact)
|
|
296
|
+
|
|
297
|
+
Four standard kernels added to make the audio story concrete. All are written so
|
|
298
|
+
the output is **bit-identical** across every engine and native target — integer
|
|
299
|
+
math (bytebeat, blur) or transcendental-free f64 (fft, synth: in-source Taylor
|
|
300
|
+
polynomials, not `Math.sin`, which differs per libm). Go's arm64 auto-FMA gives
|
|
301
|
+
the documented `fma` parity class on the f64 cases.
|
|
302
|
+
|
|
303
|
+
| case | jz | vs V8 | vs AS | vs fastest native | jz wasm |
|
|
304
|
+
| --- | ---: | ---: | ---: | --- | ---: |
|
|
305
|
+
| **synth** — osc + ADSR + biquad | **2.32 ms** | **1.33×** | **1.07×** | **beats all** (Rust 0.89×) | 2.0 kB |
|
|
306
|
+
| **fft** — radix-2 Cooley–Tukey | **1.14 ms** | **1.27×** | **1.13×** | **ties** (Rust 1.07×) | 2.3 kB |
|
|
307
|
+
| **blur** — RGBA box blur | **0.90 ms** | **9.4×** | **5.8×** | trails (native SIMDs the stencil, 1.5×) | 3.4 kB |
|
|
308
|
+
| **bytebeat** — integer one-liner | **0.67 ms** | **3.7×** | **5.4×** | trails (native vectorizes, 1.3×) | 1.5 kB |
|
|
309
|
+
|
|
310
|
+
The headline: jz beats the JS field (V8, AssemblyScript) on **every** audio/image
|
|
311
|
+
case, **ties native on FFT**, and is the **fastest of all targets on the synth
|
|
312
|
+
pipeline** — its per-sample loop is loop-carried (oscillator phase + biquad
|
|
313
|
+
feedback), so native can't auto-vectorize it either, and jz's tight scalar f64
|
|
314
|
+
codegen with no NaN-box overhead wins outright. The two stateless integer kernels
|
|
315
|
+
(bytebeat, blur) are where `clang`/`zig`/`rustc` auto-vectorize an
|
|
316
|
+
embarrassingly-parallel loop jz emits as scalar — native is the floor there, but
|
|
317
|
+
jz still beats the JS field by 3.7–9.4× there. (Numbers: darwin/arm64, Apple M4 Max; the live snapshot
|
|
318
|
+
is [results.json](results.json).)
|
|
319
|
+
|
|
281
320
|
### watr — WAT-to-wasm compiler on small corpus
|
|
282
321
|
|
|
283
322
|
| target | median | ×v8 | size | parity |
|
|
284
323
|
| --- | ---: | ---: | ---: | --- |
|
|
285
|
-
| V8 (node) raw JS | 1.
|
|
286
|
-
| **jz → V8 wasm** | **1.
|
|
324
|
+
| V8 (node) raw JS | 1.38 ms | 1.00× | 2.6 kB | ok |
|
|
325
|
+
| **jz → V8 wasm** | **1.17 ms** | **1.17×** | **238.4 kB** | **ok** |
|
|
287
326
|
|
|
288
327
|
jz is 1.07× slower than V8 raw JS on this large compiler bundle. The size
|
|
289
328
|
(144 kB) is the full jz-compiled watr parser + encoder + optimizer; V8's JIT
|
|
@@ -295,25 +334,36 @@ Aggregate geomean (jz / target):
|
|
|
295
334
|
|
|
296
335
|
| target | speed | size |
|
|
297
336
|
| --- | ---: | ---: |
|
|
298
|
-
| V8 (node) | **0.
|
|
299
|
-
| AssemblyScript | **0.
|
|
300
|
-
|
|
301
|
-
|
|
337
|
+
| V8 (node) | **0.40×** | — |
|
|
338
|
+
| AssemblyScript | **0.36×** | **1.16×** |
|
|
339
|
+
|
|
340
|
+
jz wins or ties V8 on every kernel case; the only V8 losses are the
|
|
341
|
+
self-hosting rows `watr` (1.07×) and `jessie` (1.28×). AS is beaten on
|
|
342
|
+
speed across the shared cases. On size jz is ~1.1× AS (geomean 1.16×,
|
|
343
|
+
median 1.10×) — jz wins on speed, AS on bytes.
|
|
302
344
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
345
|
+
Against the systems languages compiled to the same target — **WebAssembly, run in
|
|
346
|
+
V8** — jz is **2.4× faster than C, 2.6× than Rust, and 4.9× than Go** (geomean),
|
|
347
|
+
and **competitive with native C** itself (1.07×, the lone non-wasm reference).
|
|
348
|
+
That apples-to-apples wasm field is the headline chart above.
|
|
306
349
|
|
|
307
350
|
Case-by-case summary:
|
|
308
351
|
|
|
309
352
|
* **biquad, mat4, poly, bitwise, callback: large wins.** jz beats V8 by
|
|
310
|
-
|
|
353
|
+
2.0–12.9× and AS by 1.4–9.1×. Typed-array scalarization, i32 narrowing,
|
|
311
354
|
and closure lowering are the drivers.
|
|
312
355
|
* **tokenizer, aos, mandelbrot, sort, crc32: modest wins.** jz beats V8 by
|
|
313
|
-
1.1–2.
|
|
356
|
+
1.1–2.4× and ties or beats AS. These are memory-bound or branch-heavy
|
|
314
357
|
workloads where codegen quality matters less than data layout.
|
|
315
|
-
* **json: solid win.** jz beats V8 by 1.
|
|
358
|
+
* **json: solid win.** jz beats V8 by 1.3× on runtime JSON parsing; AS
|
|
316
359
|
cannot run this case.
|
|
360
|
+
* **alpha: the native floor.** jz beats V8 2.0× and every wasm rival (Rust/C/Go
|
|
361
|
+
→ wasm), but trails the native-C reference ~14× (374 µs vs 26 µs) — alpha's hot
|
|
362
|
+
path is an unsigned i32 multiply. jz already lifts it to 128-bit SIMD (an i16x8
|
|
363
|
+
widening byte-map); native C pulls ahead on width alone — 256-bit AVX2 with a
|
|
364
|
+
fused byte multiply-add, run as native code with no per-load bounds checks. The
|
|
365
|
+
residual is the wasm-v128-vs-AVX2 ceiling, not a missing pass. A known gap we
|
|
366
|
+
publish rather than hide.
|
|
317
367
|
* **watr: near parity.** jz is 1.07× slower than V8 on a 144 kB compiler
|
|
318
|
-
bundle.
|
|
319
|
-
jz's AOT wasm.
|
|
368
|
+
bundle. It is one of two self-host rows (with jessie) where V8's
|
|
369
|
+
profile-guided JIT tiers beat jz's AOT wasm.
|