From da219dffe3ee2a4e1c860ee38e176e0f3d71584f Mon Sep 17 00:00:00 2001 From: Perfloop Agent Date: Sat, 1 Aug 2026 23:16:08 +0000 Subject: [PATCH 1/2] Add truncated fraction benchmark --- CMakeLists.txt | 9 ++ benchmarks/bench_truncated_fraction.cpp | 183 ++++++++++++++++++++++++ 2 files changed, 192 insertions(+) create mode 100644 benchmarks/bench_truncated_fraction.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 1ec61423..33213115 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -59,6 +59,15 @@ endif() target_compile_options(fast_float INTERFACE $<$,$,19.10>>:/permissive->) +option(FASTFLOAT_TRUNCATED_FRACTION_BENCHMARK + "Build the long truncated-fraction regression benchmark" OFF) +if(FASTFLOAT_TRUNCATED_FRACTION_BENCHMARK) + add_executable(bench_truncated_fraction + benchmarks/bench_truncated_fraction.cpp) + target_link_libraries(bench_truncated_fraction PRIVATE fast_float) + target_compile_features(bench_truncated_fraction PRIVATE cxx_std_17) +endif() + if(FASTFLOAT_INSTALL) include(CMakePackageConfigHelpers) diff --git a/benchmarks/bench_truncated_fraction.cpp b/benchmarks/bench_truncated_fraction.cpp new file mode 100644 index 00000000..d6e66461 --- /dev/null +++ b/benchmarks/bench_truncated_fraction.cpp @@ -0,0 +1,183 @@ +#include "fast_float/fast_float.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr size_t integer_digits = 770; +constexpr size_t max_digits = 769; +constexpr size_t fraction_digits = 4 * 1024 * 1024; +constexpr size_t direct_iterations = 2048; +constexpr size_t from_chars_iterations = 64; +constexpr size_t zero_suffix_direct_iterations = 8192; +constexpr size_t zero_suffix_from_chars_iterations = 1024; +constexpr double expected_value = 0x0.607b00a417628p-1022; + +// The leading digits and exponent take from_chars through digit_comp. The +// final integer digit selects whether parse_mantissa must scan the 4 MiB, +// all-zero fractional suffix. + +#if defined(_MSC_VER) +#define FASTFLOAT_BENCH_NOINLINE __declspec(noinline) +#elif defined(__GNUC__) || defined(__clang__) +#define FASTFLOAT_BENCH_NOINLINE __attribute__((noinline)) +#else +#define FASTFLOAT_BENCH_NOINLINE +#endif + +struct input { + std::string text{}; + fast_float::parsed_number_string parsed{}; +}; + +[[noreturn]] void fail(char const *message) { + std::fputs(message, stderr); + std::fputc('\n', stderr); + std::exit(EXIT_FAILURE); +} + +std::string make_input(char final_integer_digit) { + std::string result = "8385788696668661046"; + result.append(integer_digits - result.size() - 1, '0'); + result.push_back(final_integer_digit); + result.push_back('.'); + result.append(fraction_digits, '0'); + result += "e-1078"; + return result; +} + +void initialize(input &value, char final_integer_digit) { + value.text = make_input(final_integer_digit); + fast_float::parse_options options; + value.parsed = fast_float::parse_number_string( + value.text.data(), value.text.data() + value.text.size(), options, true); + if (!value.parsed.valid || !value.parsed.too_many_digits || + value.parsed.integer.len() != integer_digits || + value.parsed.fraction.len() != fraction_digits) { + fail("unexpected parsed input"); + } + + double parsed_value = 0; + auto const parsed = fast_float::from_chars( + value.text.data(), value.text.data() + value.text.size(), parsed_value); + if (parsed.ec != std::errc() || + parsed.ptr != value.text.data() + value.text.size() || + parsed_value != expected_value) { + fail("unexpected conversion result"); + } +} + +FASTFLOAT_BENCH_NOINLINE uint64_t +parse_mantissa_once(fast_float::parsed_number_string const &input) { + fast_float::parsed_number_string number = input; + fast_float::bigint result; + size_t digits = 0; + fast_float::parse_mantissa(result, number, max_digits, digits); + bool truncated = false; + return result.hi64(truncated) ^ (uint64_t(digits) << 1) ^ uint64_t(truncated); +} + +FASTFLOAT_BENCH_NOINLINE uint64_t from_chars_once(std::string const &input) { + double result = 0; + auto const parsed = + fast_float::from_chars(input.data(), input.data() + input.size(), result); + if (parsed.ec != std::errc() || parsed.ptr != input.data() + input.size()) { + fail("unexpected conversion result"); + } + + uint64_t bits = 0; + static_assert(sizeof(bits) == sizeof(result), "unexpected double size"); + std::memcpy(&bits, &result, sizeof(bits)); + return bits; +} + +inline void do_not_optimize(uint64_t value) { +#if defined(__GNUC__) || defined(__clang__) + asm volatile("" : : "r"(value) : "memory"); +#else + volatile uint64_t sink = value; + (void)sink; +#endif +} + +template +double measure(std::array const &inputs, size_t iterations, + Function function) { + uint64_t sink = 0; + auto const start = std::chrono::steady_clock::now(); + for (size_t index = 0; index < iterations; ++index) { + sink += function(inputs[index & 1]); + do_not_optimize(sink); + } + auto const finish = std::chrono::steady_clock::now(); + auto const elapsed = + std::chrono::duration_cast(finish - start) + .count(); + return double(elapsed) / double(iterations); +} + +void print_measurement(double nanoseconds_per_operation) { + std::printf("{\"metric\":\"ns/op\",\"value\":%.17g}\n", + nanoseconds_per_operation); +} + +} // namespace + +int main(int argc, char **argv) { + if (argc != 2) { + std::fputs( + "usage: bench_truncated_fraction " + "{parse_mantissa|from_chars|parse_mantissa_zero_suffix|" + "from_chars_zero_suffix}\n", + stderr); + return EXIT_FAILURE; + } + + bool direct = false; + bool zero_suffix = false; + if (std::strcmp(argv[1], "parse_mantissa") == 0) { + direct = true; + } else if (std::strcmp(argv[1], "from_chars") == 0) { + direct = false; + } else if (std::strcmp(argv[1], "parse_mantissa_zero_suffix") == 0) { + direct = true; + zero_suffix = true; + } else if (std::strcmp(argv[1], "from_chars_zero_suffix") == 0) { + zero_suffix = true; + } else { + std::fputs( + "usage: bench_truncated_fraction " + "{parse_mantissa|from_chars|parse_mantissa_zero_suffix|" + "from_chars_zero_suffix}\n", + stderr); + return EXIT_FAILURE; + } + + std::array inputs{}; + initialize(inputs[0], zero_suffix ? '0' : '1'); + initialize(inputs[1], zero_suffix ? '0' : '2'); + size_t const iterations = + zero_suffix + ? (direct ? zero_suffix_direct_iterations + : zero_suffix_from_chars_iterations) + : (direct ? direct_iterations : from_chars_iterations); + + if (direct) { + print_measurement(measure(inputs, iterations, + [](input const &value) { + return parse_mantissa_once(value.parsed); + })); + } else { + print_measurement(measure(inputs, iterations, + [](input const &value) { + return from_chars_once(value.text); + })); + } + return EXIT_SUCCESS; +} From 889f0507728bde7c9b900a1475716b55c9009fb9 Mon Sep 17 00:00:00 2001 From: Perfloop Agent Date: Sat, 1 Aug 2026 23:01:47 +0000 Subject: [PATCH 2/2] Avoid scanning irrelevant truncated fraction --- include/fast_float/digit_comparison.h | 5 +- tests/basictest.cpp | 92 +++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 2 deletions(-) diff --git a/include/fast_float/digit_comparison.h b/include/fast_float/digit_comparison.h index c2c83b0c..1f8f0d46 100644 --- a/include/fast_float/digit_comparison.h +++ b/include/fast_float/digit_comparison.h @@ -285,8 +285,9 @@ parse_mantissa(bigint &result, parsed_number_string_t &num, // add the temporary value, then check if we've truncated any digits add_native(result, limb(powers_of_ten_uint64[counter]), value); bool truncated = is_truncated(p, pend); - if (num.fraction.ptr != nullptr) { - truncated |= is_truncated(num.fraction); + // A nonzero integer suffix already determines the rounding direction. + if (!truncated && num.fraction.ptr != nullptr) { + truncated = is_truncated(num.fraction); } if (truncated) { round_up_bigint(result, digits); diff --git a/tests/basictest.cpp b/tests/basictest.cpp index dba36e8a..799b43a5 100644 --- a/tests/basictest.cpp +++ b/tests/basictest.cpp @@ -1153,6 +1153,98 @@ TEST_CASE("double.inf") { std::errc::result_out_of_range); } +TEST_CASE("truncated integer mantissa") { + constexpr size_t max_digits = fast_float::binary_format::max_digits(); + constexpr size_t integer_digits = max_digits + 1; + constexpr size_t fraction_length = 1024; + + auto make_mantissa = [=](char final_integer_digit, + char final_fraction_digit) { + std::string input = "1234567890123456789"; + input.append(integer_digits - input.size() - 1, '0'); + input.push_back(final_integer_digit); + input.push_back('.'); + input.append(fraction_length - 1, '0'); + input.push_back(final_fraction_digit); + return input; + }; + + auto parse_mantissa = [=](std::string const &input, + fast_float::bigint &result) -> size_t { + fast_float::parse_options options; + auto number = fast_float::parse_number_string( + input.data(), input.data() + input.size(), options, true); + CHECK(number.valid); + CHECK(number.integer.len() == integer_digits); + CHECK(number.fraction.len() == fraction_length); + + size_t digits = 0; + fast_float::parse_mantissa(result, number, max_digits, digits); + return digits; + }; + + auto const nonzero_integer_zero_fraction = make_mantissa('1', '0'); + auto const nonzero_integer_nonzero_fraction = make_mantissa('1', '1'); + fast_float::bigint nonzero_integer_zero_result; + fast_float::bigint nonzero_integer_nonzero_result; + CHECK(parse_mantissa(nonzero_integer_zero_fraction, + nonzero_integer_zero_result) == max_digits + 1); + CHECK(parse_mantissa(nonzero_integer_nonzero_fraction, + nonzero_integer_nonzero_result) == max_digits + 1); + CHECK(nonzero_integer_zero_result.compare(nonzero_integer_nonzero_result) == + 0); + + auto const zero_integer_zero_fraction = make_mantissa('0', '0'); + auto const zero_integer_nonzero_fraction = make_mantissa('0', '1'); + fast_float::bigint zero_integer_zero_result; + fast_float::bigint zero_integer_nonzero_result; + CHECK(parse_mantissa(zero_integer_zero_fraction, + zero_integer_zero_result) == max_digits); + CHECK(parse_mantissa(zero_integer_nonzero_fraction, + zero_integer_nonzero_result) == max_digits + 1); + CHECK(zero_integer_zero_result.compare(zero_integer_nonzero_result) < 0); + + auto make_exact_conversion_input = [=](char final_fraction_digit) { + std::string input = "8385788696668661046"; + input.append(integer_digits - input.size() - 1, '0'); + input.push_back('1'); + input.push_back('.'); + input.append(fraction_length - 1, '0'); + input.push_back(final_fraction_digit); + input += "e-1078"; + return input; + }; + + auto const exact_input = make_exact_conversion_input('0'); + fast_float::parse_options options; + auto const number = fast_float::parse_number_string( + exact_input.data(), exact_input.data() + exact_input.size(), options, + true); + REQUIRE(number.too_many_digits); + REQUIRE(number.integer.len() == integer_digits); + REQUIRE(number.fraction.len() == fraction_length); + auto const approximate = + fast_float::compute_float>( + number.exponent, number.mantissa); + auto const next = fast_float::compute_float>( + number.exponent, number.mantissa + 1); + REQUIRE(approximate != next); + REQUIRE(fast_float::compute_error>( + number.exponent, number.mantissa) + .power2 < 0); + + for (char final_fraction_digit = '0'; final_fraction_digit <= '1'; + ++final_fraction_digit) { + auto const input = make_exact_conversion_input(final_fraction_digit); + double value = 0; + auto const result = fast_float::from_chars( + input.data(), input.data() + input.size(), value); + CHECK(result.ec == std::errc()); + CHECK(result.ptr == input.data() + input.size()); + CHECK(value == 0x0.607b00a417628p-1022); + } +} + TEST_CASE("double.general") { verify("0.95000000000000000000", 0.95); verify("22250738585072012e-324",