Merge pull request #432 from fastfloat/rounds-to-nearest-macro

Add FASTFLOAT_ROUNDS_TO_NEAREST to skip the rounding-mode check
This commit is contained in:
Daniel Lemire 2026-11-09 11:58:27 -05:00 committed by GitHub
commit 0b21c2eadb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 171 additions and 3 deletions

View File

@ -155,8 +155,9 @@ Furthermore, we have the following restrictions:
We support Visual Studio, macOS, Linux, freeBSD. We support big and little We support Visual Studio, macOS, Linux, freeBSD. We support big and little
endian. We support 32-bit and 64-bit systems. endian. We support 32-bit and 64-bit systems.
We assume that the rounding mode is set to nearest (`std::fegetround() == The results are rounded to the nearest floating-point value, whatever the
FE_TONEAREST`). rounding mode of the thread (`std::fegetround()`). See
[Assuming the default rounding mode](#assuming-the-default-rounding-mode).
## Integer types ## 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. 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 ## Users and Related Work
The fast_float library is part of: The fast_float library is part of:

View File

@ -74,12 +74,20 @@ from_chars_result_t<UC>
* Returns true if the floating-pointing rounding mode is to 'nearest'. * 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. * It is the default on most system. This function is meant to be inexpensive.
* Credit : @mwalcott3 * 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 { fastfloat_really_inline bool rounds_to_nearest() noexcept {
// https://lemire.me/blog/2020/06/26/gcc-not-nearest/ // https://lemire.me/blog/2020/06/26/gcc-not-nearest/
#if (FLT_EVAL_METHOD != 1) && (FLT_EVAL_METHOD != 0) #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; return false;
#endif #elif defined(FASTFLOAT_ROUNDS_TO_NEAREST)
return true;
#else
// See // See
// A fast function to check your floating-point rounding mode // 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/ // 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__) #elif defined(__GNUC__)
#pragma GCC diagnostic pop #pragma GCC diagnostic pop
#endif #endif
#endif // FASTFLOAT_ROUNDS_TO_NEAREST
} }
} // namespace detail } // namespace detail

View File

@ -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_test)
fast_float_add_cpp_test(example_comma_test) fast_float_add_cpp_test(example_comma_test)
fast_float_add_cpp_test(example_integer_times_pow10) fast_float_add_cpp_test(example_integer_times_pow10)
fast_float_add_cpp_test(rounds_to_nearest_test)
fast_float_add_cpp_test(basictest) fast_float_add_cpp_test(basictest)
option(FASTFLOAT_CONSTEXPR_TESTS "Require constexpr tests (build will fail if the compiler won't support it)" OFF) option(FASTFLOAT_CONSTEXPR_TESTS "Require constexpr tests (build will fail if the compiler won't support it)" OFF)
if (FASTFLOAT_CONSTEXPR_TESTS) if (FASTFLOAT_CONSTEXPR_TESTS)

View File

@ -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 <cfloat>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <random>
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<double> prices(-1e6, 1e6);
std::uniform_int_distribution<uint64_t> 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;
}