From 8cd1810adbd697f7cc3ede8bc4ad61d6c801e130 Mon Sep 17 00:00:00 2001 From: Daniel Lemire Date: Mon, 9 Nov 2026 11:55:36 -0500 Subject: [PATCH] Add FASTFLOAT_ROUNDS_TO_NEAREST to skip the rounding-mode check When FASTFLOAT_ROUNDS_TO_NEAREST is defined, fast_float assumes that the rounding mode is the default (round to nearest) and skips the volatile probe before Clinger's fast path. The FLT_EVAL_METHOD check is kept. The README documents the tradeoff. --- README.md | 55 ++++++++++++++- include/fast_float/parse_number.h | 11 ++- tests/CMakeLists.txt | 1 + tests/rounds_to_nearest_test.cpp | 107 ++++++++++++++++++++++++++++++ 4 files changed, 171 insertions(+), 3 deletions(-) create mode 100644 tests/rounds_to_nearest_test.cpp diff --git a/README.md b/README.md index 73a2b85..c8f9376 100644 --- a/README.md +++ b/README.md @@ -155,8 +155,9 @@ Furthermore, we have the following restrictions: We support Visual Studio, macOS, Linux, freeBSD. We support big and little endian. We support 32-bit and 64-bit systems. -We assume that the rounding mode is set to nearest (`std::fegetround() == -FE_TONEAREST`). +The results are rounded to the nearest floating-point value, whatever the +rounding mode of the thread (`std::fegetround()`). See +[Assuming the default rounding mode](#assuming-the-default-rounding-mode). ## Integer types @@ -485,6 +486,56 @@ Overloads of `fast_float::integer_times_pow10()` are provided for signed and unsigned integer types up to 64 bits: `int64_t`, `uint64_t`, etc. +## Assuming the default rounding mode + +The C and C++ languages let a program change the floating-point rounding +mode of a thread with `std::fesetround` (e.g., `FE_UPWARD`). fast_float +returns the value rounded to the nearest floating-point number (ties to +even) whatever the current rounding mode. + +Most numbers that people write, such as `1234.56` or `-0.25`, are converted +with a single multiplication or division by an exact power of ten (Clinger's +fast path). That operation is correctly rounded only in the default mode +(`FE_TONEAREST`), so fast_float checks the rounding mode before it takes the +fast path, every time. The check is cheap (a load from a `volatile` variable, +two additions and a comparison), but when you parse short numbers in a tight +loop, it can be a noticeable fraction of the time, depending on your compiler +and processor. + +If your program never changes the rounding mode, you can skip the check by +defining the `FASTFLOAT_ROUNDS_TO_NEAREST` macro: + +```C++ +#define FASTFLOAT_ROUNDS_TO_NEAREST +#include "fast_float/fast_float.h" +``` + +It affects `from_chars`, `from_chars_advanced` and `integer_times_pow10`. + +Before you define it, consider the tradeoff: + +* **You make a promise.** With the macro, fast_float trusts that the rounding + mode is `FE_TONEAREST`. If some code in your process (yours, or a library + you call) sets another mode, the numbers that take the fast path are + rounded according to that mode, while the other numbers are still rounded + to the nearest value. The results are then not correctly rounded, and not + even consistent with one another. For example, with `FE_UPWARD`, `0.3` + becomes `0.30000000000000004` but `0.30000000000000000000001` becomes + `0.29999999999999999`: the larger input gives the smaller value. Nothing + crashes and nothing warns you. Without the macro, fast_float is correct in + every rounding mode. +* **It is only a speedup on short inputs.** Long numbers (more than about 15 + significant digits, or large exponents) never take the fast path, so they + gain nothing. +* **Define it the same way everywhere.** The macro changes inline functions, + so every translation unit that includes fast_float should see the same + definition. The simplest is to set it for the whole build + (`-DFASTFLOAT_ROUNDS_TO_NEAREST`, or + `target_compile_definitions(myapp PRIVATE FASTFLOAT_ROUNDS_TO_NEAREST)` in + CMake) rather than in a source file. + +If in doubt, leave the macro undefined: the default is always correct. + ## Users and Related Work The fast_float library is part of: diff --git a/include/fast_float/parse_number.h b/include/fast_float/parse_number.h index ca74fae..0006fc4 100644 --- a/include/fast_float/parse_number.h +++ b/include/fast_float/parse_number.h @@ -74,12 +74,20 @@ from_chars_result_t * Returns true if the floating-pointing rounding mode is to 'nearest'. * It is the default on most system. This function is meant to be inexpensive. * Credit : @mwalcott3 + * + * If FASTFLOAT_ROUNDS_TO_NEAREST is defined, we trust the user that the + * rounding mode is always 'nearest' and we skip the check (see README). */ fastfloat_really_inline bool rounds_to_nearest() noexcept { // https://lemire.me/blog/2020/06/26/gcc-not-nearest/ #if (FLT_EVAL_METHOD != 1) && (FLT_EVAL_METHOD != 0) + // The arithmetic may be carried out with excess precision (e.g., x87): + // Clinger's fast path could then round twice. FASTFLOAT_ROUNDS_TO_NEAREST + // does not change this. return false; -#endif +#elif defined(FASTFLOAT_ROUNDS_TO_NEAREST) + return true; +#else // See // A fast function to check your floating-point rounding mode // https://lemire.me/blog/2022/11/16/a-fast-function-to-check-your-floating-point-rounding-mode/ @@ -133,6 +141,7 @@ fastfloat_really_inline bool rounds_to_nearest() noexcept { #elif defined(__GNUC__) #pragma GCC diagnostic pop #endif +#endif // FASTFLOAT_ROUNDS_TO_NEAREST } } // namespace detail diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 84c0bf0..a8cc62b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -67,6 +67,7 @@ fast_float_add_cpp_test(supported_chars_test) fast_float_add_cpp_test(example_test) fast_float_add_cpp_test(example_comma_test) fast_float_add_cpp_test(example_integer_times_pow10) +fast_float_add_cpp_test(rounds_to_nearest_test) fast_float_add_cpp_test(basictest) option(FASTFLOAT_CONSTEXPR_TESTS "Require constexpr tests (build will fail if the compiler won't support it)" OFF) if (FASTFLOAT_CONSTEXPR_TESTS) diff --git a/tests/rounds_to_nearest_test.cpp b/tests/rounds_to_nearest_test.cpp new file mode 100644 index 0000000..1c56b92 --- /dev/null +++ b/tests/rounds_to_nearest_test.cpp @@ -0,0 +1,107 @@ +// Checks that defining FASTFLOAT_ROUNDS_TO_NEAREST does not change the results +// when the rounding mode is the default (round to nearest). +#define FASTFLOAT_ROUNDS_TO_NEAREST +#include "fast_float/fast_float.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +bool check_double(char const *s) { + double const expected = std::strtod(s, nullptr); + double result = 0; + auto const answer = fast_float::from_chars(s, s + std::strlen(s), result); + if (answer.ec != std::errc() || + std::memcmp(&result, &expected, sizeof(double)) != 0) { + std::cerr << "mismatch on " << s << "\n"; + return false; + } + return true; +} + +bool check_float(char const *s) { + float const expected = std::strtof(s, nullptr); + float result = 0; + auto const answer = fast_float::from_chars(s, s + std::strlen(s), result); + if (answer.ec != std::errc() || + std::memcmp(&result, &expected, sizeof(float)) != 0) { + std::cerr << "mismatch on " << s << " (float)\n"; + return false; + } + return true; +} + +} // namespace + +int main() { +#if (FLT_EVAL_METHOD == 0) || (FLT_EVAL_METHOD == 1) + if (!fast_float::detail::rounds_to_nearest()) { + std::cerr << "FASTFLOAT_ROUNDS_TO_NEAREST is ignored\n"; + return EXIT_FAILURE; + } +#endif + char const *cases[] = {"0", + "-0", + "1", + "-1", + "0.1", + "-0.1", + "1e10", + "-1e10", + "1e-10", + "16777216", + "16777217", + "9007199254740992", + "9007199254740993", + "123456.789", + "-1234.56", + "1e23"}; + for (char const *s : cases) { + if (!check_double(s) || !check_float(s)) { + return EXIT_FAILURE; + } + } + char const *double_cases[] = {"1e22", "-1e22", "1e-22", + "2.2250738585072014e-308", + "1.7976931348623157e308"}; + for (char const *s : double_cases) { + if (!check_double(s)) { + return EXIT_FAILURE; + } + } + std::mt19937_64 gen(1234); + std::uniform_real_distribution prices(-1e6, 1e6); + std::uniform_int_distribution bits; + char buffer[64]; + for (size_t i = 0; i < 100000; i++) { + // short decimals: they take Clinger's fast path + std::snprintf(buffer, sizeof(buffer), "%.2f", prices(gen)); + if (!check_double(buffer) || !check_float(buffer)) { + return EXIT_FAILURE; + } + // arbitrary doubles: most take the other paths + uint64_t const word = bits(gen); + double d; + std::memcpy(&d, &word, sizeof(d)); + if (d != d || d - d != 0) { // NaN or infinity + continue; + } + std::snprintf(buffer, sizeof(buffer), "%.17g", d); + if (!check_double(buffer)) { + return EXIT_FAILURE; + } + } + if (fast_float::integer_times_pow10(uint64_t(123456), -2) != 1234.56 || + fast_float::integer_times_pow10(int64_t(-123456), -2) != -1234.56) { + std::cerr << "integer_times_pow10 mismatch\n"; + return EXIT_FAILURE; + } + std::cout << "ok\n"; + return EXIT_SUCCESS; +}