Symbolica WASM versus native wheels
Measured on 2026-10-30 with matching Symbolica 3.0.2 revisions, both reporting
symbolica-e227fe7, on an AMD Ryzen 9 5900X running Linux 7.2.11-arch1-1.
Native CPython is 3.14.8; Pyodide 314.0.7 uses CPython 3.14.2, ABI 2026_0.
Node.js is v26.12.1. Native NumPy is 2.5.4; WASM NumPy is 2.4.6.
Every row below was measured afresh in this session.
| Operation | Native | WASM | WASM/native |
|---|---|---|---|
Parse expanded (1+x+y)^30 (496 terms) |
511.1 µs | 698.7 µs | 1.37× |
Expand (1+x+y+z)^30 (5,456 terms) |
6198 µs | 9410 µs | 1.52× |
Factor x^12-y^12 |
283.9 µs | 440 µs | 1.55× |
Differentiate expanded (1+x+y+z)^30 |
6196 µs | 8792 µs | 1.42× |
| Dense polynomial multiplication (455 × 364 terms) | 799.7 µs | 1602 µs | 2.00× |
| Exact polynomial division (1,540 ÷ 120 terms) | 775.9 µs | 1264 µs | 1.63× |
| Directly evaluate 496 terms (control) | 128.7 µs | 1430 µs | 11.11× |
| Prebuilt evaluator, one point | 1.014 µs | 3.929 µs | 3.87× |
| Prebuilt evaluator, batch of 256; per point | 0.07506 µs | 2.535 µs | 33.77× |
Method
Each backend runs in three fresh processes, pinned to logical CPU 6, in order native/WASM, WASM/native, native/WASM. Each case warms up for at least five seconds, then nine calibrated batches target 120 ms each. Large differentiation has at least twenty seconds and 2,000 operations of warmup in every process. Reported values are medians of the three process medians. No samples were removed. Input construction, validation, imports, installation, and runtime startup are outside timing; parsing cases deliberately include parsing. Timings include Python loops, API calls, output allocation, and output destruction.
Optimized numerical evaluation
Both optimized rows call expanded.evaluator([x, y], n_cores=1) once and retain
the default JIT setting. The native evaluator uses JIT compilation; the WASM
build uses its optimized instruction interpreter. Each evaluator is first
called outside timing, including native JIT compilation, and subsequently
warmed up before sampling. Timed calls use preallocated, contiguous float64
NumPy arrays. Output arrays are allocated during the timed calls.
The batch row divides the complete 256-point call time by 256; the single-point
row includes the full Python-call overhead for each point.
Setup costs
These are separately recorded latencies across the same three fresh processes; they are excluded from the steady-state results above. The combined row is the median of the per-process sums, rather than the sum of the two separate medians. NumPy import and runtime startup are excluded from these setup measurements too.
| Evaluator setup | Native | WASM |
|---|---|---|
| Construct evaluator | 2.72 ms | 44.1 ms |
| First single-point call (includes native JIT) | 3.88 ms | 13.6 ms |
| Construction + first call | 6.63 ms | 55 ms |
Workloads and validation
The large parse, direct-evaluation control, and optimized evaluators use expanded
(1+x+y)^30 (496 terms). Single-point evaluation uses x=0.1, y=0.2.
The batch uses 256 varying points with x from 0.05 to 0.15 and y from 0.15
to 0.25. Each output is checked against (1+x+y)^30 at relative tolerance
1e-12, independently of the expanded evaluation program.
The direct control calls Expression.evaluate() without a compiled evaluator;
it remains in the raw data and this report, rather than the release table.
The large expansion and derivative use (1+x+y+z)^30 (5,456 terms),
differentiating in x. Multiplication uses expanded (1+x+y+z)^12 and
(1+2*x-y+3*z)^11. Division divides the product of expanded (1+x+y+z)^12
and (1+2*x-y+3*z)^7 by the latter polynomial. These polynomial inputs are
adapted from Symbolica's FLINT comparison suite; FLINT itself is not timed here.
Every process also checks expansion term counts, the complete parse round trip, factorization reconstruction, a known derivative, and exact quotient reconstruction. The complete 496-term input has the same SHA-256 in every process. The comparison includes differences in Python runtimes, numeric backends, NumPy versions, compiler settings, and WASM execution. It does not isolate any one cause of the measured differences.
Artifact provenance
The native package was recovered from its cached 3.0.2 installation. The core
WASM source wheel was restored from website Git blob
0a086dc16a1697b56d8f3ac05a5f683721b9ba70. Both report the same Symbolica
revision. Runtime binaries and their build settings were not modified.
The exact binary and wheel hashes are retained below and in the raw measurements.
The restored WASM wheel is not assumed byte-identical to the previous session's
wheel, so every displayed workload was rerun.
Native module SHA-256: 6ea87e1d0bd2ddafdfc2a30b356ba40e1122fc66280fb969eb74083482fd6890.
WASM wheel SHA-256: 77169863665a4e4d220e0db269a6766d3bb69d0d9fc996cdc96883a3c8f3572f.
WASM native module SHA-256: 5f4d00ba0483aab9a124772c5950166cfeac13d853ef72e912a451b95fd1b4d5.
Reproduce
Install a matching native Symbolica build and NumPy into an isolated Python 3.14 environment. Obtain the Pyodide 314.0.7 runtime with ABI 2026_0. Keep the three linked scripts together, then run:
export SYMBOLICA_NATIVE_PYTHON=/absolute/path/to/venv/bin/python
export PYODIDE_ROOT=/absolute/path/to/pyodide/dist
export SYMBOLICA_WASM_WHEEL=/absolute/path/to/symbolica-3.0.2-cp314-abi3-pyemscripten_2026_0_wasm32.whl
python run_wasm_vs_native.py --output /absolute/path/to/fresh-results --runs 3 --cpu 6 \
--cases parse_496_terms expand_5456_terms factor_small derivative_5456_terms \
flint_dense_multiply flint_dense_exact_divide evaluate_496_terms \
evaluator_496_terms_single evaluator_496_terms_batch256The runner checks matching Symbolica revisions and input hashes. --resume
reuses completed process JSON records; use it only with unchanged workloads,
binaries, runtime, and CPU affinity. Package downloads are outside timed regions
and cached in a temporary directory, configurable with SYMBOLICA_BENCH_CACHE.
The raw measurements embed the workload, launcher, and controller sources from
this session. The current launcher additionally keeps downloaded packages out
of the website's runtime source directory; that cache location does not change
the timed workload.
All fresh samples, setup costs, and process medians · Previous measurements · Python workload · Pyodide launcher · Process runner