Skip to content

1.1 "fixed point release"

Latest

Choose a tag to compare

@vitaut vitaut released this 20 Jul 13:21

This release adds fixed notation support, along with substantial float and SIMD performance improvements, a smaller binary footprint via optional size optimizations, a standalone C header, broad portability fixes (32-bit, clang, GCC/NEON, MSVC), and a rewritten verification and benchmarking toolchain. Overall, double-to-string conversion is roughly 23-28% faster than in 1.0 on Apple M5 Max and 28-38% on AMD EPYC Milan.

Time per double on Apple M5 Max (smaller is better), from the dtoa-benchmark:

dtoa-benchmark: time per double

New Features & API

  • Added support for fixed notation, which write automatically emits (for example 123.456 rather than 1.23456e+02) for values of moderate magnitude, switching to scientific notation for large or small magnitudes, similar to the default floating-point format in Python and other languages (#64, #111, #124, thanks @dougallj).
  • Added a dedicated C header (zmij-c.h) (#93).
  • Extended to_decimal with a negative flag, which also enables correct handling of negative zero.
  • Added the ZMIJ_OPTIMIZE_SIZE macro that enables binary size optimizations when defined to 1 (the default when compiling with __OPTIMIZE_SIZE__), reducing zmij's code and data from ~20 KB to ~4 KB (~79% reduction on Apple clang at -O2).
  • Applied Dougall Johnson's pow10 compression method when compiling in binary-size-optimized mode (#97).
  • Added the ZMIJ_USE_SIMD CMake option to control SIMD (#128, thanks @spinicist) and CMake Threads support for the tooling (#122, thanks @spinicist).

Performance & SIMD

  • Built the float string entirely inside a SIMD register for up to a ~30% speed-up (#140, thanks @TobiSchluter).
  • Added SIMD for floats and optimized the float path more broadly (#120, thanks @TobiSchluter; addresses #99, #121).
  • Assembled fixed-notation output in a single SIMD register on SSE4.1 for a ~6-8% speed-up on fixed-notation benchmarks (#137, thanks @TobiSchluter).
  • Improved the SSE4.1 fixed-double path (#110, thanks @Antares0982) and the NEON fixed-double path (#114, thanks @Antares0982).
  • Converted BCD → ASCII after shuffling for a ~3% speed-up (#112, thanks @TobiSchluter).
  • Used addition instead of OR when inserting zeros to enable LEA (#82, thanks @TobiSchluter).
  • Optimized pow10 table access to avoid keeping the index live across umul192.
  • Removed the Schubfach fallback for both the double and float paths.
  • Applied a digit-split optimization from the xjb paper and extended it across the scalar, SSE, and NEON paths.
  • Folded the leading-zero shift into the SIMD digit shuffle and reworked handling of the last digit to reduce branching.
  • Made exponent output branch-free (idea by @xjb714) and optimized it for floats.
  • Added an optional precomputed exponent table and micro-optimizations for GCC (e.g. forcing cmov, replacing poorly predicted branches).
  • Landed additional SSE improvements and ensured scalars do not affect the SIMD memory layout (#125, #129, thanks @TobiSchluter).
  • Derived length from the unshuffled value for the x86-64-v2 microarchitecture and reorganized the SSE code.
  • Forced SIMD constants to be loaded from memory rather than synthesized, improving code generation (#78, thanks @TobiSchluter).
  • Re-enabled SIMD in the C version and fixed several SIMD configuration corner cases, including building the tests with ZMIJ_USE_SIMD=0 (#127).

Portability & Toolchain Fixes

  • Fixed clang compile errors (#104, thanks @Antares0982) and GCC NEON build issues, including a build failure on Apple Silicon (#116).
  • Fixed MSVC compilation of the C version and multiple C portability issues (#83, #84, #103, thanks @AlexGuteniev, @jcol2), including MSVC ARM64 and x86/x64 warning cleanups (#126, #136).
  • Avoided a disallowed conversion from unsigned to int (#113, thanks @TobiSchluter) and renamed the C version's select to zmij_select to avoid a clash with the POSIX select function (#139, thanks @MasterDuke17).
  • Added a correct fallback for 32-bit systems and re-enabled SSE on 32-bit MSVC builds (#119, #135, fixes #132, thanks @IRainman).
  • Guarded __builtin_is_constant_evaluated for compilers such as VS2017 (#142, fixes #141, thanks @archo5).
  • Fixed various warnings in the C version (#87, #90, #92, #95, thanks @MasterDuke17, @IRainman, @himikof).
  • Applied the correct test settings (#106, thanks @TobiSchluter) and removed unnecessary null termination for doubles (#107, thanks @AlexGuteniev).

Correctness & Safety

  • Fixed a buffer underrun in the SSE fixed path (#86).
  • Fixed incorrect NaN formatting that produced -nan (#109).
  • Corrected handling of the last digit, including round-to-even behavior (#118).

Verification & Tooling

  • Added a double-to-string verification script (verify.py) that verifies zmij's rounding against the shortest correctly rounded representation for all significands of every binary exponent.
  • Reworked benchmarking:
    • Switched to Google Benchmark and improved reporting.
    • Added an A/B testing script (abtest.py) with float support and a "benchmark all commits" workflow.
    • Added a fixed-notation benchmark (Canada dataset), a float benchmark, and a results-plotting script.
    • Fixed the fixed-notation benchmark (#143, thanks @TobiSchluter).
  • Added CI for Linux, macOS, and Windows, the last in #115 (thanks @Antares0982).

New Contributors

Thanks also to @TobiSchluter, @AlexGuteniev, @dougallj, and @xjb714 for their continued contributions, and to everyone who reported issues.

Full Changelog: v1.0...v1.1