Skip to content

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_batch256

The 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