Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
name = "fpfmt"
version = "0.3.0"
edition = "2024"
rust-version = "1.95"
description = "Floating-point formatting and parsing, ported from rsc/fpfmt"
license = "BSD-3-Clause"
repository = "https://github.com/wado-lang/fpfmt"
Expand Down Expand Up @@ -35,5 +36,8 @@ lto = true
opt-level = "z"
strip = true

[profile.bench]
opt-level = 3

[dev-dependencies]
chacha8rand = "0.1"
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,23 +40,23 @@ exact at 128 bits and the low 64 bits are always zero.

At runtime, `prescale` multiplies the two factors back together with u128
arithmetic instead of doing a direct table lookup. This reduces Wasm binary
size from 14 KB to **4 KB** with a modest formatting slowdown (~1.6x),
while parsing is unaffected. Still **2.4x faster** than ryu for formatting.
size from 14 KB to **4 KB** with a modest formatting slowdown (~1.6x) and a
smaller parsing one (~1.2x). Still **~1.3x faster** than ryu for formatting.

```toml
fpfmt = { version = "0.2", features = ["small"] }
```

## Benchmarks

Formatting and parsing 8 representative f64 values (`1.0`, `0.1`, `3.14`, `PI`, `E`, `1e23`, `5e-324`, `1.7976931348623157e308`).
Formatting and parsing 8 representative f64 values (`1.0`, `0.1`, `3.14`, `PI`, `E`, `1e23`, `5e-324`, `1.7976931348623157e308`). Parse inputs are each value's shortest round-trip string.

Measured on Apple M3 Pro, macOS 15.7.3 (aarch64):
Measured on Intel Xeon (2.10 GHz), Ubuntu 24.04 (x86_64), rustc 1.95:

| Task | fpfmt | fpfmt `small` | ryu | stdlib |
| ------------------------- | -----: | ------------: | -----: | -----: |
| **format** (f64 → string) | 63 ns | 99 ns | 239 ns | 529 ns |
| **parse** (string → f64) | 690 ns | 700 ns | — | 675 ns |
| **format** (f64 → string) | 150 ns | 237 ns | 308 ns | 927 ns |
| **parse** (string → f64) | 67 ns | 80 ns | — | 113 ns |

```sh
cargo bench -p bench
Expand All @@ -67,8 +67,8 @@ cargo bench -p bench --features small

| Configuration | Size |
| ------------- | --------------: |
| default | 14,379 bytes |
| `small` | **4,175 bytes** |
| default | 14,291 bytes |
| `small` | **4,078 bytes** |

```sh
cargo build --target wasm32-unknown-unknown --release -p wasm-size
Expand Down
2 changes: 1 addition & 1 deletion rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[toolchain]
channel = "1.93"
channel = "1.95"
targets = ["wasm32-unknown-unknown"]
components = ["clippy", "rustfmt"]
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ fn unpack64(f: f64) -> (u64, i32) {
let mut m = (1u64 << 63) | ((b & ((1u64 << 52) - 1)) << SHIFT);
let mut e = ((b >> 52) & ((1u64 << SHIFT) - 1)) as i32;
if e == 0 {
core::hint::cold_path();
m &= !(1u64 << 63);
e = MIN_EXP;
let s = m.leading_zeros();
Expand Down Expand Up @@ -366,10 +367,12 @@ pub fn short(f: f64) -> (u64, i32) {
let mut z: i32 = 11; // extra zero bits at bottom of m; 11 for 53-bit m
let p;
if m == 1u64 << 63 && e > MIN_EXP {
core::hint::cold_path();
p = -skewed(e + z);
min = m - (1u64 << (z - 2) as u32); // min = m - 1/4 * 2**(e+z)
} else {
if e < MIN_EXP {
core::hint::cold_path();
z = 11 + (MIN_EXP - e);
}
p = -log10_pow2(e + z);
Expand Down
16 changes: 15 additions & 1 deletion tools/bench/benches/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,21 @@ fn bench_format(c: &mut Criterion) {
}

fn bench_parse(c: &mut Criterion) {
let strings: Vec<String> = VALUES.iter().map(|f| f.to_string()).collect();
// Parse the shortest round-trip strings that fpfmt itself emits, so the
// benchmark measures parsing valid input for both parsers. `f.to_string()`
// expands values like 5e-324 to a several-hundred-digit decimal, which
// fpfmt's 19-significant-digit parser rejects rather than parses — that
// would measure rejection speed, not parsing.
let strings: Vec<String> = VALUES
.iter()
.map(|&f| {
let (d, p) = fpfmt::short(f);
let nd = fpfmt::digits(d);
let mut buf = [0u8; 32];
let n = fpfmt::fmt_float(&mut buf, d, p, nd);
core::str::from_utf8(&buf[..n]).unwrap().to_string()
})
.collect();

let mut group = c.benchmark_group("parse");

Expand Down
Loading