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:
New Features & API
- Added support for fixed notation, which
writeautomatically emits (for example123.456rather than1.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_decimalwith a negative flag, which also enables correct handling of negative zero. - Added the
ZMIJ_OPTIMIZE_SIZEmacro 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
pow10compression method when compiling in binary-size-optimized mode (#97). - Added the
ZMIJ_USE_SIMDCMake option to control SIMD (#128, thanks @spinicist) and CMakeThreadssupport 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
pow10table access to avoid keeping the index live acrossumul192. - 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-v2microarchitecture 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
unsignedtoint(#113, thanks @TobiSchluter) and renamed the C version'sselecttozmij_selectto avoid a clash with the POSIXselectfunction (#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_evaluatedfor 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
NaNformatting 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) withfloatsupport 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
- @MasterDuke17: first contribution in #87
- @IRainman: first contribution in #90
- @himikof: first contribution in #95
- @jcol2: first contribution in #103
- @Antares0982: first contribution in #104
- @spinicist: first contribution in #122
- @archo5: first contribution in #142
Thanks also to @TobiSchluter, @AlexGuteniev, @dougallj, and @xjb714 for their continued contributions, and to everyone who reported issues.
Full Changelog: v1.0...v1.1
