From 0d5b70a95a9bc482efa5a0049d9ffe66cbdcb288 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Mon, 20 Jul 2026 20:49:27 +0000 Subject: [PATCH] Disable Clinger float fast path under extended FP precision (x87) The contiguous number fast path uses a Clinger-style exact algorithm (significand * 10^scale in double arithmetic), which is only correctly rounded when double operations are evaluated in true 53-bit precision. On the x87 FPU used by 32-bit x86 (FLT_EVAL_METHOD == 2) the single multiply/divide is computed in 80-bit and then double-rounded to double, so a small fraction of values land 1 ULP off. This surfaced as test-cbor_cpp11 and test-msgpack_cpp11 failing on the mingw (x86) job for regression/floats.json: the C++17 builds pass because they take the correctly-rounded std::from_chars path, while C++11 falls back to parse_float_fast(). A 5M-sample check over shortest round-trip decimals reproduces it: 0 divergences with 53-bit doubles, ~1 in 25 000 with 80-bit intermediates; declining to std::strtod fixes all of them. Guard parse_float_fast() on FLT_EVAL_METHOD so it declines whenever the platform evaluates doubles in extended precision, letting the caller use the correctly-rounded std::from_chars / std::strtod path instead. On mainstream x86-64/ARM64 (FLT_EVAL_METHOD == 0) the fast path is unchanged. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01AXcDtEma2PjxgmPS9cQGzA Signed-off-by: Niels Lohmann --- include/nlohmann/detail/input/number_parse.hpp | 15 +++++++++++++++ single_include/nlohmann/json.hpp | 15 +++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/include/nlohmann/detail/input/number_parse.hpp b/include/nlohmann/detail/input/number_parse.hpp index 4da712f81..29be3a67e 100644 --- a/include/nlohmann/detail/input/number_parse.hpp +++ b/include/nlohmann/detail/input/number_parse.hpp @@ -9,6 +9,7 @@ #pragma once #include // array +#include // FLT_EVAL_METHOD #include // size_t #include // int64_t, uint64_t #include // numeric_limits @@ -120,6 +121,19 @@ below). template bool parse_float_fast(const char* first, const char* last, DecimalPointType decimal_point, double& out) noexcept { +#if defined(FLT_EVAL_METHOD) && FLT_EVAL_METHOD != 0 + // Clinger's fast path is only exact when double operations are evaluated in + // true double precision. On platforms that keep intermediates in extended + // precision (e.g. the x87 FPU on 32-bit x86, where FLT_EVAL_METHOD == 2) the + // single significand * 10^scale step is double-rounded and can be 1 ULP off, + // so decline and let the caller fall back to the correctly-rounded + // std::from_chars / std::strtod path. + static_cast(first); + static_cast(last); + static_cast(decimal_point); + static_cast(out); + return false; +#else static const std::array powers_of_ten = { { @@ -236,6 +250,7 @@ bool parse_float_fast(const char* first, const char* last, DecimalPointType deci } out = negative ? -result : result; return true; +#endif } /// fast float path is only exact for `double`; decline for float/long double diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index f40630517..f7dd2b705 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -7791,6 +7791,7 @@ NLOHMANN_JSON_NAMESPACE_END #include // array +#include // FLT_EVAL_METHOD #include // size_t #include // int64_t, uint64_t #include // numeric_limits @@ -7903,6 +7904,19 @@ below). template bool parse_float_fast(const char* first, const char* last, DecimalPointType decimal_point, double& out) noexcept { +#if defined(FLT_EVAL_METHOD) && FLT_EVAL_METHOD != 0 + // Clinger's fast path is only exact when double operations are evaluated in + // true double precision. On platforms that keep intermediates in extended + // precision (e.g. the x87 FPU on 32-bit x86, where FLT_EVAL_METHOD == 2) the + // single significand * 10^scale step is double-rounded and can be 1 ULP off, + // so decline and let the caller fall back to the correctly-rounded + // std::from_chars / std::strtod path. + static_cast(first); + static_cast(last); + static_cast(decimal_point); + static_cast(out); + return false; +#else static const std::array powers_of_ten = { { @@ -8019,6 +8033,7 @@ bool parse_float_fast(const char* first, const char* last, DecimalPointType deci } out = negative ? -result : result; return true; +#endif } /// fast float path is only exact for `double`; decline for float/long double