// Glaze Library // For the license information refer to glaze.hpp // // Header-only port of Victor Zverovich's zmij library for float serialization. // https://github.com/vitaut/zmij // Distributed under the upstream MIT license // (see https://github.com/vitaut/zmij/blob/main/LICENSE) or alternatively // the Boost Software License, Version 1.0. // Copyright (c) 2025 - present, Victor Zverovich // // Adaptation notes: // - Ported from zmij.h + zmij.cc into a single header wrapped in glz::zmij. // - The upstream ZMIJ_OPTIMIZE_SIZE compile-time macro is replaced by a // `bool OptSize` template parameter so the two variants (full tables vs // recompute-on-the-fly) can coexist in one translation unit. Glaze's // `is_size_optimized(Opts)` flag drives it at the call site. // - Exposes glz::to_chars, a JSON-compliant wrapper that writes "null" for // NaN/Inf, uppercase 'E' for the exponent, and omits the '+' on positive // exponents (matching Glaze's established output format). // - Internal helpers live in glz::zmij::detail_impl (formerly an anonymous // namespace in zmij.cc); namespace-level constants that previously collided // with struct members (neg100/neg10/neg10k/zeros) are suffixed `_v`. #pragma once #include #include #include #include #include #include #include #include #ifndef ZMIJ_USE_SIMD // SIMD (SSE/NEON) is enabled by default; defining GLZ_DISABLE_SIMD selects the // scalar code paths, matching the rest of Glaze. #if defined(GLZ_DISABLE_SIMD) #define ZMIJ_USE_SIMD 0 #else #define ZMIJ_USE_SIMD 1 #endif #endif // Non-finite output format for zmij::detail::write. 0 (default) emits "null" // to stay JSON-compliant; =1 restores upstream zmij behavior ("inf" / "nan"). #ifndef GLZ_ZMIJ_EMIT_INF_NAN #define GLZ_ZMIJ_EMIT_INF_NAN 0 #endif #ifdef ZMIJ_USE_NEON #elif defined(__ARM_NEON) || defined(_M_ARM64) #define ZMIJ_USE_NEON ZMIJ_USE_SIMD #else #define ZMIJ_USE_NEON 0 #endif #if ZMIJ_USE_NEON #include #endif #ifdef ZMIJ_USE_SSE #elif defined(__SSE2__) #define ZMIJ_USE_SSE ZMIJ_USE_SIMD #elif defined(_M_AMD64) || (defined(_M_IX86_FP) && _M_IX86_FP == 2) #define ZMIJ_USE_SSE ZMIJ_USE_SIMD #else #define ZMIJ_USE_SSE 0 #endif #if ZMIJ_USE_SSE #include #endif #ifdef ZMIJ_USE_SSE4_1 static_assert(!ZMIJ_USE_SSE4_1 || ZMIJ_USE_SSE); #elif defined(__SSE4_1__) || defined(__AVX__) #define ZMIJ_USE_SSE4_1 ZMIJ_USE_SSE #else #define ZMIJ_USE_SSE4_1 0 #endif #ifdef __aarch64__ #define ZMIJ_AARCH64 1 #else #define ZMIJ_AARCH64 0 #endif #ifdef __x86_64__ #define ZMIJ_X86_64 1 #else #define ZMIJ_X86_64 0 #endif #ifdef __clang__ #define ZMIJ_CLANG 1 #else #define ZMIJ_CLANG 0 #endif #ifdef _MSC_VER #define ZMIJ_MSC_VER _MSC_VER #include #else #define ZMIJ_MSC_VER 0 #endif #if defined(__has_builtin) && !defined(ZMIJ_NO_BUILTINS) #define ZMIJ_HAS_BUILTIN(x) __has_builtin(x) #else #define ZMIJ_HAS_BUILTIN(x) 0 #endif // Size-vs-speed is controlled per call via the `OptSize` template parameter // on glz::zmij::to_chars / detail::write / to_decimal; no global macro. // Forced inlining. MSVC's __forceinline is honored on every MSVC version, // whereas Glaze's GLZ_ALWAYS_INLINE relies on [[msvc::forceinline]] which // only landed in VS 2022 17.6. Keep zmij on the older spelling to preserve // performance on the full MSVC support range. #if defined(_MSC_VER) && !defined(__clang__) #define ZMIJ_INLINE __forceinline #else #define ZMIJ_INLINE __attribute__((always_inline)) inline #endif #ifdef __GNUC__ #define ZMIJ_ASM(x) asm x #else #define ZMIJ_ASM(x) #endif #ifdef ZMIJ_CONST_DECL #elif ZMIJ_AARCH64 #define ZMIJ_CONST_DECL #else #define ZMIJ_CONST_DECL static constexpr #endif #if defined(__clang__) #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wsign-compare" #pragma clang diagnostic ignored "-Wunused-parameter" #pragma clang diagnostic ignored "-Wpedantic" #elif defined(__GNUC__) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wsign-compare" #pragma GCC diagnostic ignored "-Wunused-parameter" #pragma GCC diagnostic ignored "-Wpedantic" #elif ZMIJ_MSC_VER #pragma warning(push) // C4702: false-positive unreachable-code in MSVC 14.50 (compiler regression) // C4324: SIMD types are intentionally padded by alignas #pragma warning(disable : 4702) #pragma warning(disable : 4324) #endif namespace glz::zmij { struct dec_fp { long long sig; int exp; bool negative; }; enum { non_finite_exp = int(~0u >> 1) }; enum { float_buffer_size = 17, double_buffer_size = 34 }; namespace detail_impl { inline constexpr bool is_big_endian = std::endian::native == std::endian::big; inline auto bswap64(uint64_t x) noexcept -> uint64_t { #if ZMIJ_HAS_BUILTIN(__builtin_bswap64) return __builtin_bswap64(x); #elif ZMIJ_MSC_VER return _byteswap_uint64(x); #else return ((x & 0xff00000000000000) >> 56) | ((x & 0x00ff000000000000) >> 40) | ((x & 0x0000ff0000000000) >> 24) | ((x & 0x000000ff00000000) >> +8) | ((x & 0x00000000ff000000) << +8) | ((x & 0x0000000000ff0000) << 24) | ((x & 0x000000000000ff00) << 40) | ((x & 0x00000000000000ff) << 56); #endif } inline auto clz(uint64_t x) noexcept -> int { assert(x != 0); #if ZMIJ_HAS_BUILTIN(__builtin_clzll) return __builtin_clzll(x); #elif defined(_M_AMD64) && defined(__AVX2__) return __lzcnt64(x); #elif defined(_M_AMD64) || defined(_M_ARM64) unsigned long idx; _BitScanReverse64(&idx, x); return 63 - idx; #elif ZMIJ_MSC_VER unsigned long idx; if (_BitScanReverse(&idx, uint32_t(x >> 32))) return 31 - idx; _BitScanReverse(&idx, uint32_t(x)); return 63 - idx; #else int n = 64; for (; x > 0; x >>= 1) --n; return n; #endif } inline auto ctz(uint64_t x) noexcept -> int { assert(x != 0); #if ZMIJ_HAS_BUILTIN(__builtin_ctzll) return __builtin_ctzll(x); #elif defined(_M_AMD64) || defined(_M_ARM64) unsigned long idx; _BitScanForward64(&idx, x); return idx; #elif ZMIJ_MSC_VER unsigned long idx; if (_BitScanForward(&idx, uint32_t(x))) return idx; _BitScanForward(&idx, uint32_t(x >> 32)); return idx + 32; #else int n = 0; for (; (x & 1) == 0; x >>= 1) ++n; return n; #endif } struct uint128 { uint64_t hi; uint64_t lo; [[maybe_unused]] explicit constexpr operator uint64_t() const noexcept { return lo; } [[maybe_unused]] constexpr auto operator>>(int shift) const noexcept -> uint128 { if (shift == 32) return {hi >> 32, (hi << 32) | (lo >> 32)}; assert(shift >= 64 && shift < 128); return {0, hi >> (shift - 64)}; } }; #ifdef ZMIJ_USE_INT128 #elif defined(__SIZEOF_INT128__) #define ZMIJ_USE_INT128 1 #else #define ZMIJ_USE_INT128 0 #endif #if ZMIJ_USE_INT128 using uint128_t = unsigned __int128; #else using uint128_t = uint128; #endif #if ZMIJ_USE_INT128 && defined(__APPLE__) inline constexpr bool use_umul128_hi64 = true; #else inline constexpr bool use_umul128_hi64 = false; #endif constexpr auto umul128(uint64_t x, uint64_t y) noexcept -> uint128_t { #if ZMIJ_USE_INT128 return uint128_t(x) * y; #else #if defined(_M_AMD64) if !consteval { uint64_t hi = 0; uint64_t lo = _umul128(x, y, &hi); return {hi, lo}; } #elif defined(_M_ARM64) if !consteval { return {__umulh(x, y), x * y}; } #endif uint64_t a = x >> 32; uint64_t b = uint32_t(x); uint64_t c = y >> 32; uint64_t d = uint32_t(y); uint64_t ac = a * c; uint64_t bc = b * c; uint64_t ad = a * d; uint64_t bd = b * d; uint64_t cs = (bd >> 32) + uint32_t(ad) + uint32_t(bc); return {ac + (ad >> 32) + (bc >> 32) + (cs >> 32), (cs << 32) + uint32_t(bd)}; #endif } constexpr auto umul128_hi64(uint64_t x, uint64_t y) noexcept -> uint64_t { return uint64_t(umul128(x, y) >> 64); } inline auto umul128_add_hi64(uint64_t x, uint64_t y, uint64_t c) noexcept -> uint64_t { #if ZMIJ_USE_INT128 return uint64_t((uint128_t(x) * y + c) >> 64); #else auto p = umul128(x, y); return p.hi + (p.lo + c < p.lo); #endif } inline auto umul192_hi128(uint64_t x_hi, uint64_t x_lo, uint64_t y) noexcept -> uint128 { uint128_t p = umul128(x_hi, y); uint64_t lo = uint64_t(p) + uint64_t(umul128(x_lo, y) >> 64); return {uint64_t(p >> 64) + (lo < uint64_t(p)), lo}; } inline auto umulhi_inexact_to_odd(uint64_t x_hi, uint64_t x_lo, uint64_t y) noexcept -> uint64_t { uint128 p = umul192_hi128(x_hi, x_lo, y); return p.hi | ((p.lo >> 1) != 0); } inline auto umulhi_inexact_to_odd(uint64_t x_hi, uint64_t, uint32_t y) noexcept -> uint32_t { uint64_t p = uint64_t(umul128(x_hi, y) >> 32); return uint32_t(p >> 32) | ((uint32_t(p) >> 1) != 0); } ZMIJ_INLINE auto div10(uint64_t x) noexcept -> uint64_t { assert(x <= (1ull << 62)); constexpr uint64_t div10_sig64 = (1ull << 63) / 5 + 1; return ZMIJ_USE_INT128 ? umul128_hi64(x, div10_sig64) : x / 10; } constexpr auto compute_dec_exp(int64_t bin_exp, bool regular = true) noexcept -> int32_t { assert(bin_exp >= -1334 && bin_exp <= 2620); constexpr int log10_3_over_4_sig = 131'072; constexpr int log10_2_sig = 315'653, log10_2_exp = 20; return static_cast((bin_exp * log10_2_sig - !regular * log10_3_over_4_sig) >> log10_2_exp); } template struct float_traits : std::numeric_limits { static_assert(float_traits::is_iec559, "IEEE 754 required"); static constexpr int num_bits = float_traits::digits == 53 ? 64 : 32; static constexpr int num_sig_bits = float_traits::digits - 1; static constexpr int num_exp_bits = num_bits - num_sig_bits - 1; static constexpr int exp_mask = (1 << num_exp_bits) - 1; static constexpr int exp_bias = (1 << (num_exp_bits - 1)) - 1; static constexpr int exp_offset = exp_bias + num_sig_bits; static constexpr int min_fixed_dec_exp = -4; static constexpr int max_fixed_dec_exp = compute_dec_exp(float_traits::digits + 1) - 1; using sig_type = std::conditional_t; static constexpr sig_type implicit_bit = sig_type(1) << num_sig_bits; static auto to_bits(Float value) noexcept -> sig_type { sig_type bits; memcpy(&bits, &value, sizeof(value)); return bits; } static auto is_negative(sig_type bits) noexcept -> bool { return bits >> (num_bits - 1); } static auto get_sig(sig_type bits) noexcept -> sig_type { return bits & (implicit_bit - 1); } static auto get_exp(sig_type bits) noexcept -> int32_t { return static_cast((bits << 1) >> (num_sig_bits + 1)); } }; inline constexpr uint64_t pow10_minor[] = { 0x8000000000000000, 0xa000000000000000, 0xc800000000000000, 0xfa00000000000000, 0x9c40000000000000, 0xc350000000000000, 0xf424000000000000, 0x9896800000000000, 0xbebc200000000000, 0xee6b280000000000, 0x9502f90000000000, 0xba43b74000000000, 0xe8d4a51000000000, 0x9184e72a00000000, 0xb5e620f480000000, 0xe35fa931a0000000, 0x8e1bc9bf04000000, 0xb1a2bc2ec5000000, 0xde0b6b3a76400000, 0x8ac7230489e80000, 0xad78ebc5ac620000, 0xd8d726b7177a8000, 0x878678326eac9000, 0xa968163f0a57b400, 0xd3c21bcecceda100, 0x84595161401484a0, 0xa56fa5b99019a5c8, 0xcecb8f27f4200f3a, }; inline constexpr uint128 pow10_major[] = { {0xaf8e5410288e1b6f, 0x07ecf0ae5ee44dda}, {0xb1442798f49ffb4a, 0x99cd11cfdf41779d}, {0xb2fe3f0b8599ef07, 0x861fa7e6dcb4aa15}, {0xb4bca50b065abe63, 0x0fed077a756b53aa}, {0xb67f6455292cbf08, 0x1a3bc84c17b1d543}, {0xb84687c269ef3bfb, 0x3d5d514f40eea742}, {0xba121a4650e4ddeb, 0x92f34d62616ce413}, {0xbbe226efb628afea, 0x890489f70a55368c}, {0xbdb6b8e905cb600f, 0x5400e987bbc1c921}, {0xbf8fdb78849a5f96, 0xde98520472bdd034}, {0xc16d9a0095928a27, 0x75b7053c0f178294}, {0xc350000000000000, 0x0000000000000000}, {0xc5371912364ce305, 0x6c28000000000000}, {0xc722f0ef9d80aad6, 0x424d3ad2b7b97ef6}, {0xc913936dd571c84c, 0x03bc3a19cd1e38ea}, {0xcb090c8001ab551c, 0x5cadf5bfd3072cc6}, {0xcd036837130890a1, 0x36dba887c37a8c10}, {0xcf02b2c21207ef2e, 0x94f967e45e03f4bc}, {0xd106f86e69d785c7, 0xe13336d701beba52}, {0xd31045a8341ca07c, 0x1ede48111209a051}, {0xd51ea6fa85785631, 0x552a74227f3ea566}, {0xd732290fbacaf133, 0xa97c177947ad4096}, {0xd94ad8b1c7380874, 0x18375281ae7822bc}, }; inline constexpr uint32_t pow10_fixups[] = {0x0a4e363f, 0x00001840, 0x00006400, 0x24200040, 0x00000000, 0x0c000000, 0x82c81380, 0x5e4ce01f, 0xd730f60f, 0x0000001b, 0x00000000, 0xcdf7fffc, 0x6e8201d8, 0x40cd3fd1, 0xdb642501, 0x00000d0d, 0x14042400, 0x53713840, 0x11781db4, 0x00000000}; template struct pow10_significand_table { static constexpr bool compress = OptSize; static constexpr bool split_tables = !compress && ZMIJ_AARCH64 != 0; static constexpr int num_pow10s = 618; uint64_t data[compress ? 1 : num_pow10s * 2] = {}; static constexpr auto compute(unsigned i) noexcept -> uint128 { constexpr int stride = sizeof(pow10_minor) / sizeof(*pow10_minor); auto m = pow10_minor[(i + 10) % stride]; auto h = pow10_major[(i + 10) / stride]; uint64_t h1 = umul128_hi64(h.lo, m); uint64_t c0 = h.lo * m; uint64_t c1 = h1 + h.hi * m; uint64_t c2 = (c1 < h1) + umul128_hi64(h.hi, m); uint128 result = (c2 >> 63) != 0 ? uint128{c2, c1} : uint128{c2 << 1 | c1 >> 63, c1 << 1 | c0 >> 63}; result.lo -= (pow10_fixups[i >> 5] >> (i & 31)) & 1; return result; } constexpr pow10_significand_table() { for (int i = 0; i < num_pow10s && !compress; ++i) { uint128 result = compute(i); if (split_tables) { data[num_pow10s - i - 1] = result.hi; data[num_pow10s * 2 - i - 1] = result.lo; } else { data[i * 2] = result.hi; data[i * 2 + 1] = result.lo; } } } constexpr auto operator[](int dec_exp) const noexcept -> uint128 { constexpr int dec_exp_min = -293; int i = dec_exp - dec_exp_min; if (compress) return compute(i); if (!split_tables) return {data[i * 2], data[i * 2 + 1]}; const uint64_t* hi = data + num_pow10s + dec_exp_min - 1; const uint64_t* lo = hi + num_pow10s; if !consteval { ZMIJ_ASM(volatile("" : "+r"(hi), "+r"(lo))); } return {hi[-dec_exp], lo[-dec_exp]}; } }; constexpr ZMIJ_INLINE auto compute_exp_shift(int64_t bin_exp, int32_t dec_exp) noexcept -> int32_t { assert(dec_exp >= -350 && dec_exp <= 350); constexpr int log2_pow10_sig = 217'707, log2_pow10_exp = 16; int pow10_bin_exp = -dec_exp * log2_pow10_sig >> log2_pow10_exp; return static_cast(bin_exp + pow10_bin_exp + 1); } template struct exp_shift_table { static constexpr bool enable = !OptSize; static constexpr int extra_shift = 6; uint8_t data[enable ? float_traits::exp_mask + 1 : 1] = {}; constexpr exp_shift_table() { for (int raw_exp = 0; raw_exp < int(sizeof(data)) && enable; ++raw_exp) { int bin_exp = raw_exp - float_traits::exp_offset; if (raw_exp == 0) ++bin_exp; int dec_exp = compute_dec_exp(bin_exp); data[raw_exp] = static_cast(compute_exp_shift(bin_exp, dec_exp + 1) + extra_shift); } } }; template inline constexpr exp_shift_table exp_shifts_v; template struct exp_string_table { static constexpr bool enable = !OptSize; using traits = float_traits; static constexpr int min_dec_exp = traits::min_exponent10 - traits::max_digits10; static constexpr int offset = -min_dec_exp; uint64_t data[enable ? traits::max_exponent10 - min_dec_exp + 1 : 1] = {}; constexpr exp_string_table() { for (int e = min_dec_exp; e <= traits::max_exponent10 && enable; ++e) { uint64_t abs_e = e >= 0 ? e : -e; uint64_t bc = abs_e % 100; // Pack 1, 2, or 3 digit bytes into `val` based on magnitude. uint64_t val = bc % 10 + '0'; if (abs_e >= 10) val = (val << 8) | (bc / 10 + '0'); if (uint64_t a = abs_e / 100) val = (val << 8) | (a + '0'); uint64_t digits = abs_e >= 100 ? 3 : (abs_e >= 10 ? 2 : 1); // Glaze emits uppercase 'E', omits '+' on positive exponents, and // drops the leading zero on single-digit exponents (E7, not E07) // to minimize bytes while staying within JSON's grammar. if (e >= 0) { uint64_t len = 1 + digits; // 'E' + digits data[e + offset] = (len << 48) | (val << 8) | 'E'; } else { uint64_t len = 2 + digits; // 'E' + '-' + digits data[e + offset] = (len << 48) | (val << 16) | (uint64_t('-') << 8) | 'E'; } } } }; template inline constexpr exp_string_table exp_strings_v; struct dec_exp_format_table { using traits = float_traits; static constexpr int num_entries = traits::max_fixed_dec_exp - traits::min_fixed_dec_exp + 2; struct entry { uint8_t start_pos; uint8_t point_pos; uint8_t shift_pos; uint8_t exp_pos[traits::max_digits10]; }; entry data[num_entries] = {}; constexpr dec_exp_format_table() { for (int dec_exp = traits::min_fixed_dec_exp; dec_exp <= traits::max_fixed_dec_exp + 1; ++dec_exp) { auto& e = data[dec_exp - traits::min_fixed_dec_exp]; bool neg_fixed = dec_exp >= traits::min_fixed_dec_exp && dec_exp <= -1; bool pos_fixed = dec_exp >= 0 && dec_exp <= traits::max_fixed_dec_exp; e.start_pos = static_cast(neg_fixed ? 1 - dec_exp : 0); e.point_pos = static_cast(pos_fixed ? 1 + dec_exp : 1); e.shift_pos = static_cast(e.point_pos + (dec_exp >= 0 || dec_exp < traits::min_fixed_dec_exp)); for (int s = 1; s <= traits::max_digits10; ++s) { if (neg_fixed) e.exp_pos[s - 1] = static_cast(s); else if (pos_fixed) e.exp_pos[s - 1] = static_cast(s > dec_exp + 1 ? s + 1 : dec_exp + 1); else e.exp_pos[s - 1] = static_cast(s + 1 - (s == 1)); } } } template constexpr auto get(int dec_exp) const noexcept -> const entry& { constexpr auto min = traits::min_fixed_dec_exp, max = Traits::max_fixed_dec_exp; unsigned i = unsigned(dec_exp - min); return data[i <= unsigned(max - min) ? i : num_entries - 1]; } }; inline constexpr dec_exp_format_table dec_exp_formats; inline auto count_trailing_nonzeros(uint64_t x) noexcept -> int { if (is_big_endian) x = bswap64(x); return (size_t(70) - clz((x << 1) | 1)) / 8; } inline auto digits2(size_t value) noexcept -> const char* { alignas(2) static const char data[] = "0001020304050607080910111213141516171819" "2021222324252627282930313233343536373839" "4041424344454647484950515253545556575859" "6061626364656667686970717273747576777879" "8081828384858687888990919293949596979899"; return &data[value * 2]; } inline constexpr int div10k_exp = 40; inline constexpr uint32_t div10k_sig = uint32_t((1ull << div10k_exp) / 10000 + 1); inline constexpr uint32_t neg10k_v = uint32_t((1ull << 32) - 10000); inline constexpr int div100_exp = 19; inline constexpr uint32_t div100_sig = (1 << div100_exp) / 100 + 1; inline constexpr uint32_t neg100_v = (1 << 16) - 100; inline constexpr int div10_exp = 10; inline constexpr uint32_t div10_sig = (1 << div10_exp) / 10 + 1; inline constexpr uint32_t neg10_v = (1 << 8) - 10; inline constexpr uint64_t zeros_v = 0x0101010101010101u * '0'; inline auto write_if(char* buffer, uint32_t digit, bool condition) noexcept -> char* { *buffer = char('0' + digit); return buffer + condition; } template struct constants { static constexpr auto splat64(uint64_t x) -> uint128 { return {x, x}; } static constexpr auto splat32(uint32_t x) -> uint128 { return splat64(uint64_t(x) << 32 | x); } static constexpr auto splat16(uint16_t x) -> uint128 { return splat32(uint32_t(x) << 16 | x); } static constexpr auto pack8(uint8_t a, uint8_t b, uint8_t c, uint8_t d, // uint8_t e, uint8_t f, uint8_t g, uint8_t h) -> uint64_t { using u64 = uint64_t; return u64(h) << 56 | u64(g) << 48 | u64(f) << 40 | u64(e) << 32 | u64(d) << 24 | u64(c) << 16 | u64(b) << +8 | u64(a); } ZMIJ_CONST_DECL uint64_t threshold = 1'000'000'000'000'000ull; ZMIJ_CONST_DECL uint64_t biased_half = (uint64_t(1) << 63) + 6; #if ZMIJ_USE_NEON static constexpr int32_t neg10k = -10000 + 0x10000; using int32x4 = std::conditional_t; using int16x8 = std::conditional_t; uint64_t mul_const = 0xabcc77118461cefd; uint64_t hundred_million = 100000000; int32x4 multipliers32 = {div10k_sig, neg10k, div100_sig << 12, neg100_v}; int16x8 multipliers16 = {0xce0, neg10_v}; #elif ZMIJ_USE_SSE uint128 div100 = splat32(div100_sig); uint128 div10 = splat16((1 << 16) / 10 + 1); #if ZMIJ_USE_SSE4_1 uint128 neg100 = splat32(neg100_v); uint128 neg10 = splat16((1 << 8) - 10); uint128 bswap = uint128{pack8(15, 14, 13, 12, 11, 10, 9, 8), pack8(7, 6, 5, 4, 3, 2, 1, 0)}; #else uint128 hundred = splat32(100); uint128 moddiv10 = splat16(10 * (1 << 8) - 1); #endif uint128 div10k = splat64(div10k_sig); uint128 neg10k = splat64(neg10k_v); uint128 zeros = splat64(zeros_v); #endif alignas(64) pow10_significand_table pow10_significands; }; template alignas(64) inline constexpr constants consts_v; #if ZMIJ_USE_NEON template ZMIJ_INLINE auto to_bcd_4x4(int32x4_t ddee_bbcc_hhii_ffgg, const constants& c) noexcept -> uint8x16_t { ZMIJ_ASM(("" : "+w"(ddee_bbcc_hhii_ffgg))); int32x4_t dd_bb_hh_ff = vqdmulhq_n_s32(ddee_bbcc_hhii_ffgg, c.multipliers32[2]); int16x8_t ee_dd_cc_bb_ii_hh_gg_ff = vreinterpretq_s16_s32(vmlaq_n_s32(ddee_bbcc_hhii_ffgg, dd_bb_hh_ff, c.multipliers32[3])); int16x8_t high_10s = vqdmulhq_n_s16(ee_dd_cc_bb_ii_hh_gg_ff, c.multipliers16[0]); return vreinterpretq_u8_s16(vmlaq_n_s16(ee_dd_cc_bb_ii_hh_gg_ff, high_10s, c.multipliers16[1])); } template ZMIJ_INLINE auto to_unshuffled_digits(uint64_t value, const constants& c) -> uint8x16_t { uint64_t hundred_million = c.hundred_million; ZMIJ_ASM(("" : "+r"(hundred_million))); uint64_t abbccddee = uint64_t(umul128(value, c.mul_const) >> 90); uint64_t ffgghhii = value - abbccddee * hundred_million; uint64x1_t ffgghhii_bbccddee_64 = {reverse_hi_lo ? (abbccddee << 32) | ffgghhii : (ffgghhii << 32) | abbccddee}; int32x2_t bbccddee_ffgghhii = vreinterpret_s32_u64(ffgghhii_bbccddee_64); int32x2_t bbcc_ffgg = vreinterpret_s32_u32( vshr_n_u32(vreinterpret_u32_s32(vqdmulh_n_s32(bbccddee_ffgghhii, c.multipliers32[0])), 9)); int32x2_t ddee_bbcc_hhii_ffgg_32 = vmla_n_s32(bbccddee_ffgghhii, bbcc_ffgg, c.multipliers32[1]); // AArch64 accepts `vshll_n_u16(x, 0)` as "widen without shift", but // ARMv7 VSHLL requires a shift in [1, 15]. vmovl_u16 is the portable // name for the same operation and codegens identically on AArch64. int32x4_t ddee_bbcc_hhii_ffgg = vreinterpretq_s32_u32(vmovl_u16(vreinterpret_u16_s32(ddee_bbcc_hhii_ffgg_32))); return to_bcd_4x4(ddee_bbcc_hhii_ffgg, c); } #elif ZMIJ_USE_SSE using m128ptr = const __m128i*; // Extract the low 64 bits of an SSE register. _mm_cvtsi128_si64 targets a // 64-bit GPR and is x86-64 only (MSVC does not define it for 32-bit x86), // so on 32-bit targets fall back to a MOVQ store, which is plain SSE2 and // produces the same value. The cvt path is kept on x64 to avoid the extra // store/load round-trip there. ZMIJ_INLINE auto lo_u64(__m128i x) noexcept -> uint64_t { #if defined(__x86_64__) || defined(_M_X64) return uint64_t(_mm_cvtsi128_si64(x)); #else uint64_t result; _mm_storel_epi64(reinterpret_cast<__m128i*>(&result), x); return result; #endif } template ZMIJ_INLINE auto to_bcd_4x4(__m128i y, const constants& c) noexcept -> __m128i { const __m128i div100 = _mm_load_si128(m128ptr(&c.div100)); const __m128i div10 = _mm_load_si128(m128ptr(&c.div10)); #if ZMIJ_USE_SSE4_1 const __m128i neg100 = _mm_load_si128(m128ptr(&c.neg100)); const __m128i neg10 = _mm_load_si128(m128ptr(&c.neg10)); __m128i z = _mm_add_epi64(y, _mm_mullo_epi32(neg100, _mm_srli_epi32(_mm_mulhi_epu16(y, div100), 3))); return _mm_add_epi16(z, _mm_mullo_epi16(neg10, _mm_mulhi_epu16(z, div10))); #else const __m128i hundred = _mm_load_si128(m128ptr(&c.hundred)); const __m128i moddiv10 = _mm_load_si128(m128ptr(&c.moddiv10)); __m128i y_div_100 = _mm_srli_epi16(_mm_mulhi_epu16(y, div100), 3); __m128i y_mod_100 = _mm_sub_epi16(y, _mm_mullo_epi16(y_div_100, hundred)); __m128i z = _mm_or_si128(_mm_slli_epi32(y_mod_100, 16), y_div_100); return _mm_sub_epi16(_mm_slli_epi16(z, 8), _mm_mullo_epi16(moddiv10, _mm_mulhi_epu16(z, div10))); #endif } template ZMIJ_INLINE auto to_unshuffled_digits(uint32_t bbccddee, uint32_t ffgghhii, const constants& c) noexcept -> __m128i { const __m128i div10k = _mm_load_si128(m128ptr(&c.div10k)); const __m128i neg10k = _mm_load_si128(m128ptr(&c.neg10k)); __m128i x = _mm_set_epi64x(bbccddee, ffgghhii); __m128i y = _mm_add_epi64(x, _mm_mul_epu32(neg10k, _mm_srli_epi64(_mm_mul_epu32(x, div10k), div10k_exp))); return to_bcd_4x4(y, c); } #endif struct bcd_result { uint64_t bcd; int len; }; template inline auto to_bcd8(uint64_t abcdefgh) noexcept -> bcd_result { if (!ZMIJ_USE_SSE && !ZMIJ_USE_NEON) { uint64_t abcd_efgh = abcdefgh + neg10k_v * ((abcdefgh * div10k_sig) >> div10k_exp); uint64_t ab_cd_ef_gh = abcd_efgh + neg100_v * (((abcd_efgh * div100_sig) >> div100_exp) & 0x7f0000007f); uint64_t a_b_c_d_e_f_g_h = ab_cd_ef_gh + neg10_v * (((ab_cd_ef_gh * div10_sig) >> div10_exp) & 0xf000f000f000f); uint64_t bcd = is_big_endian ? a_b_c_d_e_f_g_h : bswap64(a_b_c_d_e_f_g_h); return {bcd, count_trailing_nonzeros(bcd)}; } const auto* c = &consts_v; ZMIJ_ASM(("" : "+r"(c))); #if ZMIJ_USE_NEON uint64_t abcd_efgh_64 = abcdefgh + neg10k_v * ((abcdefgh * div10k_sig) >> div10k_exp); int32x4_t abcd_efgh = vcombine_s32(vreinterpret_s32_u64(vcreate_u64(abcd_efgh_64)), vdup_n_s32(0)); uint8x16_t digits_128 = to_bcd_4x4(abcd_efgh, *c); uint8x8_t digits = vget_low_u8(digits_128); uint64_t bcd = vget_lane_u64(vreinterpret_u64_u8(vrev64_u8(digits)), 0); return {bcd, count_trailing_nonzeros(bcd)}; #elif ZMIJ_USE_SSE4_1 uint64_t abcd_efgh = abcdefgh + neg10k_v * ((abcdefgh * div10k_sig) >> div10k_exp); uint64_t unshuffled_bcd = lo_u64(to_bcd_4x4(_mm_set_epi64x(0, abcd_efgh), *c)); int len = unshuffled_bcd ? 8 - ctz(unshuffled_bcd) / 8 : 0; return {bswap64(unshuffled_bcd), len}; #elif ZMIJ_USE_SSE uint64_t abcd_efgh = (abcdefgh << 32) - uint64_t((10000ull << 32) - 1) * ((abcdefgh * div10k_sig) >> div10k_exp); uint64_t bcd = lo_u64(to_bcd_4x4(_mm_set_epi64x(0, abcd_efgh), *c)); return {bcd, count_trailing_nonzeros(bcd)}; #endif } template struct dec_digits { uint64_t digits; int num_digits; }; template <> struct dec_digits<64> { #if ZMIJ_USE_NEON uint16x8_t digits; #elif ZMIJ_USE_SSE __m128i digits; #else uint128 digits; #endif int num_digits; }; template ZMIJ_INLINE auto to_digits(uint64_t value, [[maybe_unused]] bool extra_digit, [[maybe_unused]] const constants& c) noexcept -> dec_digits { if constexpr (num_bits == 32) { auto result = to_bcd8(value); return {result.bcd + zeros_v, result.len}; } else { #if !ZMIJ_USE_NEON && !ZMIJ_USE_SSE uint32_t bbccddee = uint32_t(value / 100'000'000); uint32_t ffgghhii = uint32_t(value % 100'000'000); auto hi = to_bcd8(bbccddee); if (ffgghhii == 0) return {{hi.bcd + zeros_v, zeros_v}, hi.len}; auto lo = to_bcd8(ffgghhii); return {{hi.bcd + zeros_v, lo.bcd + zeros_v}, 8 + lo.len}; #elif ZMIJ_USE_NEON auto unshuffled_digits = to_unshuffled_digits(value, c); uint8x16_t digits = vrev64q_u8(unshuffled_digits); uint16x8_t str = vaddq_u16(vreinterpretq_u16_u8(digits), vreinterpretq_u16_s8(vdupq_n_s8('0'))); // vcgtzq_s8 is AArch64-only; the portable spelling (ARMv7 NEON and // up) is "compare greater-than against a zero vector". uint16x8_t is_not_zero = vreinterpretq_u16_u8(vcgtq_s8(vreinterpretq_s8_u8(digits), vdupq_n_s8(0))); uint64_t zeroes = vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(is_not_zero, 4)), 0); return {str, 16 - (clz(zeroes) >> 2)}; #else uint32_t abbccddee = uint32_t(value / 100'000'000); uint32_t ffgghhii = uint32_t(value % 100'000'000); const __m128i zeros = _mm_load_si128(m128ptr(&c.zeros)); auto unshuffled_bcd = to_unshuffled_digits(abbccddee, ffgghhii, c); #if ZMIJ_USE_SSE4_1 const __m128i bswap = _mm_load_si128(m128ptr(&c.bswap)); auto bcd = _mm_shuffle_epi8(unshuffled_bcd, bswap); #else auto bcd = _mm_shuffle_epi32(unshuffled_bcd, _MM_SHUFFLE(0, 1, 2, 3)); #endif __m128i mask128 = _mm_cmpgt_epi8(bcd, _mm_setzero_si128()); uint64_t mask = _mm_movemask_epi8(mask128); #if defined(__LZCNT__) && !defined(ZMIJ_NO_BUILTINS) int len = 32 - _lzcnt_u32(mask); #else int len = 63 - clz((mask << 1) | 1); #endif return {_mm_or_si128(bcd, zeros), len}; #endif } } struct to_decimal_result { int64_t sig; int32_t exp; int32_t last_digit = 0; bool has_last_digit = false; }; template ZMIJ_INLINE auto to_decimal(UInt bin_sig, int64_t raw_exp, bool regular, const constants& c) noexcept -> to_decimal_result { using traits = float_traits; int64_t bin_exp = raw_exp - traits::exp_offset; constexpr int num_bits = std::numeric_limits::digits; constexpr uint64_t log10_2_sig = 78'913; constexpr int32_t log10_2_exp = 18; int32_t dec_exp = 0; if constexpr (use_umul128_hi64) { dec_exp = static_cast(umul128_hi64(static_cast(bin_exp), log10_2_sig << (64 - log10_2_exp))); } else { dec_exp = compute_dec_exp(bin_exp); } uint64_t even = 1 - (bin_sig & 1); constexpr int32_t extra_shift = exp_shift_table::extra_shift; if (!regular) [[unlikely]] { int32_t irregular_dec_exp = compute_dec_exp(bin_exp, false); int32_t shift = compute_exp_shift(bin_exp, irregular_dec_exp + 1) + extra_shift; uint128 pow10 = c.pow10_significands[-irregular_dec_exp - 1]; uint128 p = umul192_hi128(pow10.hi, pow10.lo, bin_sig << shift); int64_t integral = p.hi >> extra_shift; uint64_t fractional = p.hi << (64 - extra_shift) | p.lo >> extra_shift; uint64_t half_ulp = pow10.hi >> (extra_shift + 1 - shift); bool round_up = half_ulp > ~uint64_t(0) - fractional; bool round_down = (half_ulp >> 1) > fractional; integral += round_up; int32_t digit = static_cast(umul128_add_hi64(fractional, 10, (uint64_t(1) << 63) - 1)); int32_t lo = static_cast(umul128_add_hi64(fractional - (half_ulp >> 1), 10, ~uint64_t(0))); if (digit < lo) digit = lo; return {integral, irregular_dec_exp, digit, (round_up + round_down) == 0}; } if constexpr (num_bits == 32) { constexpr int32_t float_extra_shift = 34; int32_t shift = compute_exp_shift(bin_exp, dec_exp + 1) + float_extra_shift; uint64_t pow10_hi = c.pow10_significands[-dec_exp - 1].hi; uint64_t p = umul128_hi64(pow10_hi + 1, uint64_t(bin_sig) << shift); int64_t integral = p >> float_extra_shift; uint64_t fractional = p & ((1ull << float_extra_shift) - 1); uint64_t half_ulp = (pow10_hi >> (65 - shift)) + even; bool round_up = (fractional + half_ulp) >> float_extra_shift; bool round_down = half_ulp > fractional; integral += round_up; uint64_t prod = fractional * 10; int32_t digit = static_cast(prod >> float_extra_shift); uint64_t rem = prod & ((1ull << float_extra_shift) - 1); digit += rem > (1ull << (float_extra_shift - 1)) || (rem == (1ull << (float_extra_shift - 1)) && (digit & 1)); return {integral, dec_exp, digit, (round_up + round_down) == 0}; } else { int32_t shift = exp_shift_table::enable ? exp_shifts_v.data[bin_exp + traits::exp_offset] : compute_exp_shift(bin_exp, dec_exp + 1) + extra_shift; ZMIJ_ASM(("" : "+r"(dec_exp))); uint128 pow10 = c.pow10_significands[-dec_exp - 1]; uint128 p = umul192_hi128(pow10.hi, pow10.lo, bin_sig << shift); int64_t integral = p.hi >> extra_shift; uint64_t fractional = p.hi << (64 - extra_shift) | p.lo >> extra_shift; uint64_t half_ulp = (pow10.hi >> (extra_shift + 1 - shift)) + even; bool round_up = fractional + half_ulp < fractional; bool round_down = half_ulp > fractional; integral += round_up; int32_t digit = static_cast(umul128_add_hi64(fractional, 10, c.biased_half)); if (fractional == (1ull << 62)) [[unlikely]] { digit = 2; } return {integral, dec_exp, digit, (round_up + round_down) == 0}; } } } // namespace detail_impl template inline auto to_decimal(double value) noexcept -> dec_fp { using namespace detail_impl; using traits = float_traits; auto bits = traits::to_bits(value); auto bin_exp = traits::get_exp(bits); auto bin_sig = traits::get_sig(bits); auto negative = traits::is_negative(bits); if (bin_exp == 0 || bin_exp == traits::exp_mask) [[unlikely]] { if (bin_exp != 0) return {int64_t(bin_sig), int(~0u >> 1), negative}; if (bin_sig == 0) return {0, 0, negative}; bin_exp = 1; bin_sig |= traits::implicit_bit; } auto dec = detail_impl::to_decimal( bin_sig ^ traits::implicit_bit, bin_exp, bin_sig != 0, consts_v); auto last_digit = dec.has_last_digit ? dec.last_digit : 0; return {dec.sig * 10 + last_digit, dec.exp, negative}; } namespace detail { template inline auto write(Float value, char* buffer) noexcept -> char* { using namespace detail_impl; using traits = float_traits; auto bits = traits::to_bits(value); auto bin_exp = traits::get_exp(bits); auto bin_sig = traits::get_sig(bits); *buffer = '-'; buffer += traits::is_negative(bits); const auto* c = &consts_v; ZMIJ_ASM(("" : "+r"(c))); int64_t threshold = traits::num_bits == 64 ? static_cast(c->threshold) : 10'000'000; to_decimal_result dec; bool is_normal = unsigned(bin_exp - 1) < unsigned(traits::exp_mask - 1); if (!is_normal) [[unlikely]] { if (bin_exp != 0) { #if GLZ_ZMIJ_EMIT_INF_NAN memcpy(buffer, bin_sig == 0 ? "inf" : "nan", 4); return buffer + 3; #else // Undo the speculative '-' for sign-bit-set NaN/Inf: JSON "null" // never carries a sign. buffer -= traits::is_negative(bits); memcpy(buffer, "null", 4); return buffer + 4; #endif } if (bin_sig == 0) { memcpy(buffer, "0", 2); return buffer + 1; } dec = detail_impl::to_decimal(bin_sig, 1, true, *c); int64_t dec_sig = dec.sig * 10 + (dec.has_last_digit ? dec.last_digit : 0); int32_t dec_exp = dec.exp; while (dec_sig < threshold) { dec_sig *= 10; --dec_exp; } int64_t d = static_cast(detail_impl::div10(static_cast(dec_sig))); int32_t last_digit = static_cast(dec_sig - d * 10); dec = {d, dec_exp, last_digit, last_digit != 0}; } else { dec = detail_impl::to_decimal(bin_sig | traits::implicit_bit, bin_exp, bin_sig != 0, *c); } bool extra_digit = dec.sig >= threshold; int dec_exp = dec.exp + traits::max_digits10 - 2 + extra_digit; if (traits::num_bits == 32 && dec.sig < 1'000'000) [[unlikely]] { dec.sig = 10 * dec.sig + (dec.has_last_digit ? dec.last_digit : 0); dec.has_last_digit = false; --dec_exp; } char* start = buffer; auto dig = to_digits(static_cast(dec.sig), extra_digit, *c); constexpr int bcd_size = traits::num_bits == 64 ? 16 : 8; if (dec_exp >= traits::min_fixed_dec_exp && dec_exp <= traits::max_fixed_dec_exp) { memcpy(start, &zeros_v, 8); const auto& fmt = dec_exp_formats.get(dec_exp); buffer += fmt.start_pos; memcpy(buffer, &dig.digits, bcd_size); memmove(buffer, buffer + !extra_digit, bcd_size); buffer[bcd_size + extra_digit - 1] = static_cast('0' + (dec.has_last_digit ? dec.last_digit : 0)); memmove(start + fmt.shift_pos, start + fmt.point_pos, bcd_size); start[fmt.point_pos] = '.'; int num_digits = dec.has_last_digit ? bcd_size : dig.num_digits - 1; return buffer + fmt.exp_pos[num_digits + extra_digit - 1]; } buffer += extra_digit; memcpy(buffer, &dig.digits, bcd_size); buffer[bcd_size] = static_cast('0' + dec.last_digit); buffer += dec.has_last_digit ? bcd_size + 1 : dig.num_digits; start[0] = start[1]; start[1] = '.'; buffer -= (buffer - 1 == start + 1); if constexpr (exp_string_table::enable) { uint64_t exp_data = exp_strings_v.data[dec_exp + exp_string_table::offset]; int len = int(exp_data >> 48); if (is_big_endian) exp_data = bswap64(exp_data); memcpy(buffer, &exp_data, traits::max_exponent10 >= 100 ? 8 : 4); return buffer + len; } // Glaze emits uppercase 'E', omits '+' on positive exponents, and // drops the leading zero on single-digit exponents (E7, not E07). // Branchless: write "E-" always, then advance past the '-' only when // the exponent is negative. bool neg = dec_exp < 0; buffer[0] = 'E'; buffer[1] = '-'; buffer += 1 + unsigned(neg); dec_exp = neg ? -dec_exp : dec_exp; unsigned hundreds_written = 0; if constexpr (traits::max_exponent10 >= 100) { int32_t digit = 0; if constexpr (use_umul128_hi64) { digit = static_cast(umul128_hi64(static_cast(dec_exp), 0x290000000000000ull)); } else { digit = static_cast((uint32_t(dec_exp) * div100_sig) >> div100_exp); } *buffer = static_cast('0' + digit); hundreds_written = unsigned(dec_exp >= 100); buffer += hundreds_written; dec_exp -= digit * 100; } // dec_exp now in [0, 99]. Skip the tens digit only when no higher // digit has been written (i.e. true leading zero, not interior). const char* d2 = digits2(dec_exp); *buffer = d2[0]; buffer += hundreds_written | unsigned(dec_exp >= 10); *buffer = d2[1]; return buffer + 1; } } // namespace detail } // namespace glz::zmij namespace glz { // JSON-compliant float serializer. Caller must provide a buffer of at least // zmij::float_buffer_size (17) bytes for float, zmij::double_buffer_size // (34) bytes for double. Non-finite inputs emit "null" (gated by // GLZ_ZMIJ_EMIT_INF_NAN). // // OptSize=false (default): uses the full ~17 KB pow-10 lookup tables for peak throughput. // OptSize=true: drops the tables (recomputed on the fly) for size-constrained // builds; wired to Glaze's is_size_optimized(Opts) flag. // Both instantiations can coexist in a single binary. // // Returns a pointer past the last written character. Nothing is promised // about *end or bytes after it. zmij's branchless fixed-point shuffle may // use the documented buffer capacity as scratch and leave extra bytes past // the returned range. Callers who need a null-terminated C-string must write // '\0' at *end themselves. Glaze's JSON writer doesn't care: // buffer_traits::finalize resizes the output down to the tracked length, // discarding anything past it. template inline char* to_chars(char* buf, T val) noexcept { static_assert(std::is_same_v || std::is_same_v); return zmij::detail::write(val, buf); } } #if defined(__clang__) #pragma clang diagnostic pop #elif defined(__GNUC__) #pragma GCC diagnostic pop #elif ZMIJ_MSC_VER #pragma warning(pop) #endif