Skip to content

feat: switch grisu2 float-to-string algorithm to hybrid of xjb & zmij algorithms - #3025

Open
JairusSW wants to merge 52 commits into
AssemblyScript:mainfrom
JairusSW:jairus/switch-to-xjb
Open

JairusSW wants to merge 52 commits into
AssemblyScript:mainfrom
JairusSW:jairus/switch-to-xjb

Conversation

@JairusSW

@JairusSW JairusSW commented Jun 9, 2026 •

Copy link
Copy Markdown
Member

Fixes #3012.

Changes proposed in this pull request:

Note: I wrote xjb-as with claude assisting me for an initial port. I then optimized by hand. Everything is carefully checked and passes over a trillion fuzz cases compared directly against Number::parse() in V8. It also reaches 100% code coverage and tests every edge case both I and claude could think of.

It's good quality stuff though, and I'm quite confident in that.

Current implementation measurements from independent review of 0c115329 against b6bda05, including the exact-integer string path. These are diagnostic results for the stated engine, machine, runtime, and workloads.

Generated Wasm size for tiny modules exporting f64.toString(), f32.toString(), or both, compiled with -O --noAssert --runtime stub:

Types used Base PR Increase
f64 only 4,122 B 7,319 B +3,197 B
f32 only 4,120 B 5,690 B +1,570 B
Both 4,342 B 10,690 B +6,348 B

With the default incremental runtime, the both-width module grows from 6,834 B to 13,248 B (+6,414 B). These small formatter-focused modules do not estimate whole-application growth. The generated string wrappers call shared buffered formatters; the exact-integer direct-allocation path adds specialized code.

Throughput on Node 24.22.1, x86-64 AMD EPYC 9V74, default incremental runtime, scalar builds: 4,096 inputs per bucket, 150,000 warmup operations, and medians of seven alternating base/PR samples of 400,000 operations. The timed loops run inside Wasm.

Input bucket API Base, ns/value PR, ns/value Speedup
Small signed integers f64 buffered 43.6 10.1 4.31×
Small signed integers f32 buffered 46.1 9.8 4.71×
Small signed integers f64 string 89.6 50.6 1.77×
Small signed integers f32 string 85.1 48.4 1.76×
Ordinary decimals f64 buffered 52.5 38.6 1.36×
Ordinary decimals f32 buffered 55.0 27.5 2.00×
Ordinary decimals f64 string 96.0 96.4 Effectively unchanged
Ordinary decimals f32 string 100.9 85.5 1.18×
Random finite bit patterns f64 buffered 82.9 48.0 1.73×
Random finite bit patterns f32 buffered 68.6 27.7 2.47×
Random finite bit patterns f64 string 132.1 93.1 1.42×
Random finite bit patterns f32 string 132.6 87.4 1.52×

Ordinary decimals are nonintegral values roughly between −1,000 and +1,000, formed by dividing integers by 1,000. Gains are strongest for buffered formatting and integers; complete-string performance varies with workload. The 96.0 → 96.4 ns ordinary-decimal f64 result is effectively flat, not a meaningful measured regression.

A separate SIMD diagnostic run improved random-finite buffered formatting by 2.31× (f64) and 2.75× (f32) against its own interleaved baseline. Ordinary-decimal f64 string output improved by about 6% in that run.

Correctness validation at this revision includes exact BigInt binary32 rounding intervals with ties-to-even, the four formerly double-rounded f32 regressions, all four checked-in debug/release × scalar/SIMD configurations (556,880 exact output comparisons), 28-code-unit canaries, the float-array join allocation budget, and independent verification of all 618 cached powers. Independent review also checked 28,076 cases against C++ std::to_chars(float, scientific) and 12,384 additional boundary comparisons across the buffered, ordinary string, and low-level string paths. These are substantial checks, not an exhaustive proof over all floating-point bit patterns.

  • I've read the contributing guidelines
  • I've added my name and email to the NOTICE file

P.S. Sorry about the diff. Most of it is generated WAT fixtures.

JairusSW added 3 commits June 8, 2026 21:45
Signed-off-by: Jairus Tanaka <me@jairus.dev>
Signed-off-by: Jairus Tanaka <me@jairus.dev>
Signed-off-by: Jairus Tanaka <me@jairus.dev>
@JairusSW
JairusSW marked this pull request as draft June 9, 2026 06:04
@JairusSW
JairusSW marked this pull request as ready for review June 17, 2026 18:13
@PaperPrototype

Copy link
Copy Markdown

Just curious here, why the exception of a trailing .0?

@JairusSW

JairusSW commented Jun 17, 2026 •

Copy link
Copy Markdown
Member Author

Just curious here, why the exception of a trailing .0?

Max and Dan made that decision so that floats are easily identifiable when converting them to a string. For example, you know that the string "1.0" is a result of f64.toString() while the string "1" is likely a result of i32.toString(). Slight deviation from ECMA262-spec, but it's not awful

@MaxGraey

MaxGraey commented Jun 17, 2026 •

Copy link
Copy Markdown
Member

Can you rename xjb.ts to dtoa.ts and in comments mentioned about implementation based on xjb and zmij? xjb filename is very exotic and doesn't explains exactly what this file does

Comment thread std/assembly/util/dtoa.ts Resolved
Comment thread std/assembly/util/xjb.ts Resolved
Comment thread std/assembly/util/dtoa.ts Resolved
Comment thread std/assembly/util/dtoa.ts Resolved
Comment thread std/assembly/util/dtoa.ts Resolved
JairusSW and others added 2 commits June 17, 2026 16:23
Co-authored-by: Max Graey <maxgraey@gmail.com>
Co-authored-by: Max Graey <maxgraey@gmail.com>
Comment thread std/assembly/util/dtoa.ts Resolved
Comment thread std/assembly/util/dtoa.ts Resolved
Comment thread std/assembly/util/dtoa.ts Resolved
Comment thread std/assembly/util/dtoa.ts Resolved
Comment thread std/assembly/util/dtoa.ts Resolved
Co-authored-by: Max Graey <maxgraey@gmail.com>
Comment thread tests/compiler/bindings/raw.release.wat Resolved
Comment thread std/assembly/util/dtoa.ts Resolved
Co-authored-by: Max Graey <maxgraey@gmail.com>
Comment thread std/assembly/util/dtoa.ts Resolved
Comment thread std/assembly/util/dtoa.ts Resolved
Comment thread std/assembly/util/dtoa.ts Resolved
@MaxGraey

Copy link
Copy Markdown
Member

Also plz add extra tests which cover new implementation-specific edge cases (I anready found a bug) and uncomment existing test cases which failed with grisu2

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Task] Replace Grisu2 with Xjb for float-to-string conversion

4 participants