Skip to content

Releases: vitaut/zmij

1.1 "fixed point release"

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

1.0 "exponentially fast"

Choose a tag to compare

@vitaut vitaut released this 22 Jan 14:09
e5ddbe9

What’s Changed

This release focuses on correctness and performance, providing a minimal, safe API to obtain the shortest correctly rounded decimal representation in either exponential format or as a decimal floating-point number.

Performance & Algorithm Improvements

  • Optimized division, modulo, and logarithm computations
  • Reduced conditional branching compared to Schubfach
  • Simplified decimal significand selection by using a single shorter candidate, based on an idea by Cassio Neri
  • Simplified and optimized modified rounding computation
  • Applied an optimization by Yaoyuan Guo, replacing 2–3 costly 128×64 multiplications with a single multiplication in the common case
  • Reworked digit generation to process eight digits at a time instead of using lookup tables, reducing branching and enabling better compiler optimization for more consistent performance (#4, thanks @TobiSchluter)
  • Switched to BCD encoding to evaluate eight significand digits in parallel (#7, thanks @TobiSchluter and @xjb714)
  • Made exponent handling branch-free (#10, #16, thanks @TobiSchluter)
  • Switched to built-in leading-zero counting with a safe fallback for older compilers (#21, #22, #25, thanks @AlexGuteniev)
  • Applied a collection of improvements from Dougall Johnson (#49):
    • Further optimized division, modulo, and logarithm computations
    • Optimized exponent output logic
    • Generated the powers-of-10 table using constexpr and 192-bit arithmetic
    • Applied faster indexed loads on ARM to improve table access performance
    • Introduced an optional precomputed exp_shift table to speed up decimal scaling
  • Peeled off the rightmost digit to enable cheaper division and remove zero checks, reducing branching and streamlining digit extraction (#72, thanks @TobiSchluter)
  • Replaced abs with a ternary expression to recover ~10% performance on GCC (#66, thanks @TobiSchluter)
  • Reduced conditional branching on ARM to improve performance (#73, thanks @xjb714)
  • Optimized NEON zero-check logic to speed up digit processing (#74, thanks @xjb714)

SIMD & Architecture Support

  • Added an optimized write_significand implementation using NEON to accelerate digit extraction on supported platforms (thanks @dougallj)
  • Added SSE SIMD support on x86 to leverage 128-bit vector instructions for faster parallel digit processing and improved performance (#59, thanks @TobiSchluter)
  • Enabled NEON vectorization on ARM64 MSVC (#55, thanks @AlexGuteniev)
  • Disabled SIMD correctly when ZMIJ_USE_SIMD=0 (#75, thanks @TobiSchluter)

Portability & Toolchain Fixes

  • Fixed MSVC support on x64 and ARM64 by improving code generation, replacing unavailable intrinsics, and resolving related warnings (#8, thanks @mmozeiko)
  • Fixed multiple MSVC issues across 32-bit builds, table generation, warning cleanup, vector type handling, and forced inlining (#30, #31, #34, #42, #44, #48, #50, #65, #69, #71, #76, thanks @AlexGuteniev)
  • Fixed compilation regressions after recent changes (#38, #45, #56, #57, #77, thanks @AlexGuteniev and @TobiSchluter)
  • Fixed GCC compilation issues related to NEON intrinsics (#53)

API & Usability

  • Added to_decimal for converting binary floating-point values to decimal (#6)
  • Added float (binary32) support (#1, #15)
  • Returned the size of the resulting representation (#32, thanks @AlexGuteniev)
  • Reduced float_buffer_size from 17 to 16 (#51, #52, thanks @dtolnay)
  • Trimmed leading zeros in float formatting (#27, thanks @dtolnay)
  • Lowered minimum required standard to C++14 (#61, #62, thanks @AlexGuteniev)

Correctness & Safety

  • Added assertion to countl_zero for non-zero input (#29, thanks @AlexGuteniev)
  • Completed fallback implementation for bswap64 (#23, thanks @AlexGuteniev)
  • Avoided subtracting unrelated pointers for small buffers (#36, thanks @AlexGuteniev)
  • Prevented use of hundreds in float exponent path (#43, thanks @AlexGuteniev)
  • Fixed handling of subnormals (#11, #17, #19)
  • Fixed incorrect formatting of 32-bit infinity and NaN values (#70)

Verification & Tooling

  • Added test coverage for major configurations: default (C++), no SIMD, no builtins, and C
  • Added verification programs
  • Parallelized the verification program to run across multiple threads, dividing the test space by hardware concurrency to speed up exhaustive correctness checking (#26, thanks @dtolnay)
  • Improved verification tooling:
    • Increased concurrency support (#33)
    • Made failures return non-zero exit codes (#37)
    • Added buffer overrun tests (#46, #54)
      (thanks @AlexGuteniev)
  • Fixed CSV writer usage (#60, thanks @TobiSchluter)

New Contributors

Full Changelog:
https://github.com/vitaut/zmij/commits/v1.0