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
52 changes: 41 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,16 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-14, windows-latest]
python: ["3.12", "3.13", "3.14", "3.14t", "3.15", "3.15t"]
exclude:
# 3.15 is still pre-release (alpha at time of writing) and the
# free-threaded Windows binary distributed via setup-python's
# python-versions repo has flaked here at least once
# (concurrent jobs racing for the same download). Re-add this
# cell once 3.15t has shipped a stable Windows release; until
# then ubuntu and macos provide free-threaded 3.15 coverage.
- os: windows-latest
python: "3.15t"
python: ["3.13", "3.14", "3.14t", "3.15", "3.15t"]
steps:
- uses: actions/checkout@v5

- uses: actions/setup-python@v6
with:
# Every entry is a final release (3.15.0 shipped 2026-10-09;
# setup-python's manifest carries 3.15 and 3.15t for all three
# OSes), so no allow-prereleases.
python-version: ${{ matrix.python }}
allow-prereleases: true

- name: Install build deps and package
run: |
Expand All @@ -53,3 +46,40 @@ jobs:
run: |
python -m pytest tests/cpython_suite --tb=short || true
shell: bash

test-all-backends:
# Every optional backend (fmt, ryu_opt, zmij, wuffs, ffc, stock) compiled
# in, so tests/test_backends.py can check each one against stock.
# The wheels only ship the defaults; this job is what keeps the
# others honest — and, on all three OSes, what proves the pure-C
# zmij.c / zmij_dtoa.c build with MSVC and Apple clang as well.
name: test / all backends / py3.14 / ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-14, windows-latest]
steps:
- uses: actions/checkout@v5

- uses: actions/setup-python@v6
with:
python-version: "3.14"

- name: Install with every backend compiled in
# bash on every OS: the default Windows shell is PowerShell, where
# the backslash continuations below are not line continuations.
shell: bash
run: |
python -m pip install --upgrade pip
python -m pip install ".[test]" \
-C cmake.define.FLOATIUM_FORMAT_BACKEND=all \
-C cmake.define.FLOATIUM_PARSE_BACKEND=all

- name: Show info
run: |
python -c "import floatium; print(floatium.info())"

- name: Run unit + parity + edge + backend tests
run: |
python -m pytest tests -x --tb=short --ignore=tests/cpython_suite
1 change: 0 additions & 1 deletion .github/workflows/cpython-suite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ jobs:
- uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python }}
allow-prereleases: true

- run: python -m pip install --upgrade pip && python -m pip install .

Expand Down
18 changes: 8 additions & 10 deletions .github/workflows/wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
with: { python-version: "3.12" }
with: { python-version: "3.13" }
- run: |
python -m pip install --upgrade pip build
python -m build --sdist --outdir dist
Expand All @@ -40,18 +40,16 @@ jobs:
steps:
- uses: actions/checkout@v5

- uses: pypa/cibuildwheel@v3.4.1
- name: Build wheels (cp313, cp314, cp314t, cp315, cp315t)
uses: pypa/cibuildwheel@v4.3.1
env:
# Build config lives in [tool.cibuildwheel] in pyproject.toml.
# cpython-freethreading is still required in cibuildwheel
# 3.4.1 for the cp313t / cp314t selectors — without it they
# fail with "Invalid build selector ... wasn't enabled". The
# token emits a deprecation warning (it is being renamed in a
# future cibuildwheel) but still works and is mandatory here.
# No cpython-prerelease: the build list is all released
# CPython (3.15 is omitted — 3.4.1 has no config for it).
# cibuildwheel 4.x: cp315/cp315t are in the default catalog
# (3.15.0 final since 4.3.1) and free-threaded 3.14+ builds
# need no `enable` token — the old cpython-freethreading enable
# was removed in 4.0.0 along with the 3.13t builds (which is
# why there are no cp313t wheels from 0.15.0 on).
CIBW_BUILD_VERBOSITY: 1
CIBW_ENABLE: cpython-freethreading

- uses: actions/upload-artifact@v5
with:
Expand Down
66 changes: 38 additions & 28 deletions BENCHMARKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,20 @@

Nanoseconds per operation for `repr(float)`, fixed-precision f-string
formatting, and `float(str)`, comparing stock CPython against every
floatium format backend (`fmt_opt`, `fmt`, `ryu_opt`) and every parse
backend (`fast_float`, `wuffs`, `stock`).
floatium format backend (`fmt_opt`, `fmt`, `ryu_opt`, `zmij`) and every parse
backend (`fast_float`, `wuffs`, `ffc`, `stock`).

## Methodology

- **floatium:** 0.14.2, release build, all backends compiled in.
- **floatium:** 0.16.0 (zmij a9d6363, ffc.h v26.09.01, fmt 12.2.0,
fast_float v8.3.2), release build, all backends compiled in.
- **CPython:** 3.14.3.
- **Date:** 2026-05-16.
- **Date:** 2026-10-19.
- **Script:** `FLOATIUM_AUTOPATCH=0 python -m bench.bench_backend_matrix`.
- **Corpora:** the five canonical corpora from `bench/corpora.py` —
`random_uniform`, `random_bits`, `financial`, `scientific`,
`integer_valued`; 2000 values each.
- **Timing:** mirrors `bench/bench_ns_per_op.py` — 9 outer samples per
- **Timing:** same as `bench/bench_ns_per_op.py` — 9 outer samples per
cell, each timing 5 inner loops over the whole corpus; the reported
figure is the median of the fastest third (3 of 9), robust to
background jitter without picking the cold-cache outlier.
Expand All @@ -28,30 +30,38 @@ backend (`fast_float`, `wuffs`, `stock`).
## Format backends

`repr(x)` and `f"{x:.4f}"` over each corpus. Speedup is `stock / backend`;
values above 1.00x are faster than stock.

| Corpus | Operation | Stock (ns) | fmt_opt | fmt | ryu_opt | fmt_opt speedup | fmt speedup | ryu_opt speedup |
|---|---|---:|---:|---:|---:|---:|---:|---:|
| random_uniform | `repr(x)` | 285.3 | 97.9 | 98.3 | 96.4 | 2.92x | 2.90x | 2.96x |
| random_uniform | `f"{x:.4f}"` | 119.2 | 103.4 | 103.5 | 104.8 | 1.15x | 1.15x | 1.14x |
| random_bits | `repr(x)` | 819.6 | 135.5 | 135.2 | 146.1 | 6.05x | 6.06x | 5.61x |
| random_bits | `f"{x:.4f}"` | 1,956.3 | 193.9 | 5,549.3 | 212.2 | 10.09x | 0.35x | 9.22x |
| financial | `repr(x)` | 172.0 | 80.7 | 79.6 | 101.6 | 2.13x | 2.16x | 1.69x |
| financial | `f"{x:.4f}"` | 144.5 | 100.5 | 100.1 | 115.1 | 1.44x | 1.44x | 1.25x |
| scientific | `repr(x)` | 639.8 | 133.6 | 132.7 | 141.0 | 4.79x | 4.82x | 4.54x |
| scientific | `f"{x:.4f}"` | 1,078.6 | 158.1 | 2,972.4 | 170.7 | 6.82x | 0.36x | 6.32x |
| integer_valued | `repr(x)` | 143.1 | 88.7 | 88.2 | 80.7 | 1.61x | 1.62x | 1.77x |
| integer_valued | `f"{x:.4f}"` | 168.0 | 104.8 | 104.5 | 107.6 | 1.60x | 1.61x | 1.56x |
values above 1.00x are faster than stock. `zmij` is the pure-C
single-library backend proposed for CPython; its `repr` carries a text
re-read of ~10 ns that a to-decimal entry point in Żmij's C API would
remove, and its fixed-precision path on huge magnitudes (`random_bits`)
is bigint-based and ~2x slower than Ryu's `d2fixed`, while still 4x
faster than stock.

| Corpus | Operation | Stock (ns) | fmt_opt | fmt | ryu_opt | zmij | fmt_opt speedup | fmt speedup | ryu_opt speedup | zmij speedup |
|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|
| random_uniform | `repr(x)` | 284.6 | 91.0 | 90.6 | 93.8 | 100.7 | 3.13x | 3.14x | 3.03x | 2.83x |
| random_uniform | `f"{x:.4f}"` | 119.2 | 97.9 | 97.6 | 102.9 | 109.8 | 1.22x | 1.22x | 1.16x | 1.09x |
| random_bits | `repr(x)` | 821.9 | 130.9 | 129.0 | 146.1 | 153.3 | 6.28x | 6.37x | 5.62x | 5.36x |
| random_bits | `f"{x:.4f}"` | 1,936.1 | 193.6 | 5,550.4 | 213.6 | 452.8 | 10.00x | 0.35x | 9.07x | 4.28x |
| financial | `repr(x)` | 169.5 | 73.5 | 73.5 | 99.2 | 89.3 | 2.30x | 2.31x | 1.71x | 1.90x |
| financial | `f"{x:.4f}"` | 143.5 | 95.2 | 96.0 | 114.1 | 114.0 | 1.51x | 1.49x | 1.26x | 1.26x |
| scientific | `repr(x)` | 633.8 | 127.6 | 126.9 | 142.1 | 149.7 | 4.97x | 5.00x | 4.46x | 4.23x |
| scientific | `f"{x:.4f}"` | 1,079.2 | 159.3 | 2,973.1 | 172.2 | 279.4 | 6.77x | 0.36x | 6.27x | 3.86x |
| integer_valued | `repr(x)` | 141.8 | 81.8 | 82.0 | 78.0 | 92.4 | 1.73x | 1.73x | 1.82x | 1.54x |
| integer_valued | `f"{x:.4f}"` | 167.6 | 98.2 | 99.0 | 107.2 | 109.3 | 1.71x | 1.69x | 1.56x | 1.53x |

## Parse backend

`float(s)` over each corpus, `s` being a stock CPython repr. Speedup is
`stock / backend`.

| Corpus | Operation | Stock (ns) | fast_float | wuffs | stock | fast_float speedup | wuffs speedup | stock speedup |
|---|---|---:|---:|---:|---:|---:|---:|---:|
| random_uniform | `float(s)` | 121.4 | 43.9 | 55.6 | 106.3 | 2.76x | 2.18x | 1.14x |
| random_bits | `float(s)` | 277.6 | 59.1 | 293.4 | 208.7 | 4.70x | 0.95x | 1.33x |
| financial | `float(s)` | 36.8 | 37.0 | 39.8 | 78.0 | 0.99x | 0.92x | 0.47x |
| scientific | `float(s)` | 213.7 | 57.4 | 66.5 | 183.3 | 3.72x | 3.21x | 1.17x |
| integer_valued | `float(s)` | 42.8 | 42.0 | 46.6 | 64.1 | 1.02x | 0.92x | 0.67x |
`stock / backend`. `ffc` (ffc.h, the C99 port of fast_float) is the
pure-C parse backend proposed for CPython; it tracks `fast_float`
within noise, while `wuffs` loses on 17-digit inputs (`random_bits`)
where its high-precision-decimal fallback runs.

| Corpus | Operation | Stock (ns) | fast_float | wuffs | ffc | stock | fast_float speedup | wuffs speedup | ffc speedup | stock speedup |
|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|
| random_uniform | `float(s)` | 120.8 | 45.6 | 56.2 | 44.2 | 109.3 | 2.65x | 2.15x | 2.73x | 1.11x |
| random_bits | `float(s)` | 276.5 | 58.8 | 293.4 | 59.5 | 212.1 | 4.70x | 0.94x | 4.65x | 1.30x |
| financial | `float(s)` | 36.6 | 37.6 | 39.9 | 37.3 | 79.9 | 0.98x | 0.92x | 0.98x | 0.46x |
| scientific | `float(s)` | 214.3 | 57.1 | 67.3 | 56.9 | 187.7 | 3.75x | 3.18x | 3.77x | 1.14x |
| integer_valued | `float(s)` | 43.0 | 43.4 | 47.9 | 44.1 | 66.4 | 0.99x | 0.90x | 0.98x | 0.65x |
44 changes: 36 additions & 8 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
cmake_minimum_required(VERSION 3.20)
project(floatium LANGUAGES C CXX)

set(CMAKE_C_STANDARD 11) # zmij.c: static_assert, stdbool
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)

# --- Backend selection ------------------------------------------------------
# Format backend: fmt_opt (default — fmt for repr/exp, Ryu d2fixed for fixed),
# Format backend: zmij (default — pure-C Żmij for every mode; one unmodified
# upstream library, the CPython-proposal backend),
# fmt_opt (fmt for repr/exp, Ryu d2fixed for fixed),
# fmt (plain fmt for everything, for A/B measurement),
# ryu_opt (pure-C: Ryu d2s for repr, d2exp for %e/%g, d2fixed
# for %f — zero C++ in the format path),
# all.
# Parse backend: fast_float (default), wuffs (pure-C Eisel-Lemire + HPD),
# Parse backend: ffc (default — pure-C ffc.h, the C99 port of
# fast_float; the CPython-proposal backend),
# fast_float (the C++ original), wuffs (pure-C Eisel-Lemire + HPD),
# stock (libc strtod via floatium's wrapper), all.
#
# For a "stock CPython" format baseline, just `floatium.uninstall()` —
Expand All @@ -23,10 +29,13 @@ set(CMAKE_POSITION_INDEPENDENT_CODE ON)
#
# With "all", every backend is compiled in and the active one is chosen at
# runtime via env vars FLOATIUM_FORMAT_BACKEND / FLOATIUM_PARSE_BACKEND.
set(FLOATIUM_FORMAT_BACKEND "fmt_opt" CACHE STRING "Format backend: fmt_opt, fmt, ryu_opt, all")
set(FLOATIUM_PARSE_BACKEND "fast_float" CACHE STRING "Parse backend: fast_float, wuffs, stock, all")
set_property(CACHE FLOATIUM_FORMAT_BACKEND PROPERTY STRINGS fmt_opt fmt ryu_opt all)
set_property(CACHE FLOATIUM_PARSE_BACKEND PROPERTY STRINGS fast_float wuffs stock all)
# The defaults changed from fmt_opt + fast_float to zmij + ffc in 0.16.0:
# the shipped wheel is now the exact pure-C configuration proposed for
# CPython, so every user runs the candidate code.
set(FLOATIUM_FORMAT_BACKEND "zmij" CACHE STRING "Format backend: fmt_opt, fmt, ryu_opt, zmij, all")
set(FLOATIUM_PARSE_BACKEND "ffc" CACHE STRING "Parse backend: fast_float, wuffs, ffc, stock, all")
set_property(CACHE FLOATIUM_FORMAT_BACKEND PROPERTY STRINGS fmt_opt fmt ryu_opt zmij all)
set_property(CACHE FLOATIUM_PARSE_BACKEND PROPERTY STRINGS fast_float wuffs ffc stock all)

# --- Python -----------------------------------------------------------------
# scikit-build-core passes Python3_EXECUTABLE via the FindPython hint file.
Expand Down Expand Up @@ -66,6 +75,14 @@ if(FLOATIUM_FORMAT_BACKEND STREQUAL "ryu_opt" OR FLOATIUM_FORMAT_BACKEND STREQUA
set(FLOATIUM_HAS_RYU_OPT_FORMAT 1)
set(FLOATIUM_NEEDS_RYU_D2FIXED 1)
endif()
if(FLOATIUM_FORMAT_BACKEND STREQUAL "zmij" OR FLOATIUM_FORMAT_BACKEND STREQUAL "all")
list(APPEND FLOATIUM_SRCS
src/backends/zmij_format.cc
src/cpython_adapter/zmij_dtoa.c
third_party/zmij/zmij.c
)
set(FLOATIUM_HAS_ZMIJ_FORMAT 1)
endif()
# Ryu d2fixed.c is shared between fmt_opt and ryu_opt — compile it once.
if(FLOATIUM_NEEDS_RYU_D2FIXED)
list(APPEND FLOATIUM_SRCS third_party/ryu/d2fixed.c)
Expand All @@ -84,6 +101,13 @@ if(FLOATIUM_PARSE_BACKEND STREQUAL "wuffs" OR FLOATIUM_PARSE_BACKEND STREQUAL "a
)
set(FLOATIUM_HAS_WUFFS_PARSE 1)
endif()
if(FLOATIUM_PARSE_BACKEND STREQUAL "ffc" OR FLOATIUM_PARSE_BACKEND STREQUAL "all")
list(APPEND FLOATIUM_SRCS
src/backends/ffc_parse.cc
src/cpython_adapter/ffc_strtod.c
)
set(FLOATIUM_HAS_FFC_PARSE 1)
endif()
if(FLOATIUM_PARSE_BACKEND STREQUAL "stock" OR FLOATIUM_PARSE_BACKEND STREQUAL "all")
list(APPEND FLOATIUM_SRCS src/backends/stock_parse.cc)
set(FLOATIUM_HAS_STOCK_PARSE 1)
Expand All @@ -98,6 +122,8 @@ target_include_directories(_ext PRIVATE
third_party/fast_float
third_party/ryu
third_party/wuffs
third_party/zmij
third_party/ffc
)

# Match the build flags the CPython fmt-fastfloat branch uses for the
Expand Down Expand Up @@ -128,19 +154,21 @@ endif()
# wheel but still need a single default; prefer the optimized fmt-based one.
set(FLOATIUM_DEFAULT_FORMAT "${FLOATIUM_FORMAT_BACKEND}")
if(FLOATIUM_DEFAULT_FORMAT STREQUAL "all")
set(FLOATIUM_DEFAULT_FORMAT "fmt_opt")
set(FLOATIUM_DEFAULT_FORMAT "zmij")
endif()
set(FLOATIUM_DEFAULT_PARSE "${FLOATIUM_PARSE_BACKEND}")
if(FLOATIUM_DEFAULT_PARSE STREQUAL "all")
set(FLOATIUM_DEFAULT_PARSE "fast_float")
set(FLOATIUM_DEFAULT_PARSE "ffc")
endif()

target_compile_definitions(_ext PRIVATE
$<$<BOOL:${FLOATIUM_HAS_FMT_FORMAT}>:FLOATIUM_HAS_FMT_FORMAT=1>
$<$<BOOL:${FLOATIUM_HAS_FMT_OPT_FORMAT}>:FLOATIUM_HAS_FMT_OPT_FORMAT=1>
$<$<BOOL:${FLOATIUM_HAS_RYU_OPT_FORMAT}>:FLOATIUM_HAS_RYU_OPT_FORMAT=1>
$<$<BOOL:${FLOATIUM_HAS_ZMIJ_FORMAT}>:FLOATIUM_HAS_ZMIJ_FORMAT=1>
$<$<BOOL:${FLOATIUM_HAS_FAST_FLOAT_PARSE}>:FLOATIUM_HAS_FAST_FLOAT_PARSE=1>
$<$<BOOL:${FLOATIUM_HAS_WUFFS_PARSE}>:FLOATIUM_HAS_WUFFS_PARSE=1>
$<$<BOOL:${FLOATIUM_HAS_FFC_PARSE}>:FLOATIUM_HAS_FFC_PARSE=1>
$<$<BOOL:${FLOATIUM_HAS_STOCK_PARSE}>:FLOATIUM_HAS_STOCK_PARSE=1>
FLOATIUM_DEFAULT_FORMAT_BACKEND="${FLOATIUM_DEFAULT_FORMAT}"
FLOATIUM_DEFAULT_PARSE_BACKEND="${FLOATIUM_DEFAULT_PARSE}"
Expand Down
Loading
Loading