mirror of
https://github.com/fastfloat/fast_float.git
synced 2026-10-31 03:16:25 +08:00
Merge b2c714a1b280b152235b85eae1050097d3bc5e9d into 33785d5d61993b8c22eacc58108e1cf109eca490
This commit is contained in:
commit
e0e62f1ba3
@ -49,6 +49,17 @@ target_include_directories(
|
||||
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
|
||||
)
|
||||
target_compile_features(fast_float INTERFACE cxx_std_11)
|
||||
|
||||
# This focused benchmark has no external dependencies, so it can be built
|
||||
# independently of the data-driven benchmark suite below.
|
||||
option(FASTFLOAT_TRAILING_ZERO_BENCHMARK
|
||||
"Build the trailing decimal zero slow-path benchmark" OFF)
|
||||
if(FASTFLOAT_TRAILING_ZERO_BENCHMARK)
|
||||
add_executable(trailing_zero_benchmark benchmarks/trailing_zero_benchmark.cpp)
|
||||
target_link_libraries(trailing_zero_benchmark PRIVATE fast_float)
|
||||
target_compile_features(trailing_zero_benchmark PRIVATE cxx_std_11)
|
||||
endif()
|
||||
|
||||
if(FASTFLOAT_SANITIZE)
|
||||
target_compile_options(fast_float INTERFACE -fsanitize=address -fno-omit-frame-pointer -fsanitize=undefined -fno-sanitize-recover=all)
|
||||
target_link_libraries(fast_float INTERFACE -fsanitize=address -fno-omit-frame-pointer -fsanitize=undefined -fno-sanitize-recover=all)
|
||||
|
||||
319
benchmarks/trailing_zero_benchmark.cpp
Normal file
319
benchmarks/trailing_zero_benchmark.cpp
Normal file
@ -0,0 +1,319 @@
|
||||
#include "fast_float/fast_float.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
struct input_case {
|
||||
std::string input;
|
||||
std::string canonical;
|
||||
};
|
||||
|
||||
struct target_case {
|
||||
fast_float::parsed_number_string number;
|
||||
fast_float::adjusted_mantissa am;
|
||||
|
||||
target_case() : number(), am() {}
|
||||
};
|
||||
|
||||
volatile uint64_t sink = 0;
|
||||
|
||||
std::string nonzero_tail(size_t length) {
|
||||
std::string result(length, '1');
|
||||
result.front() = '7';
|
||||
result.back() = '7';
|
||||
return result;
|
||||
}
|
||||
|
||||
bool make_target_case(std::string const &input, target_case &target) {
|
||||
fast_float::parse_options options;
|
||||
fast_float::parsed_number_string const parsed =
|
||||
fast_float::parse_number_string<false, char>(
|
||||
input.data(), input.data() + input.size(), options, true);
|
||||
fast_float::adjusted_mantissa am =
|
||||
fast_float::compute_float<fast_float::binary_format<double>>(
|
||||
parsed.exponent, parsed.mantissa);
|
||||
if (!parsed.valid || !parsed.too_many_digits || am.power2 < 0 ||
|
||||
am == fast_float::compute_float<fast_float::binary_format<double>>(
|
||||
parsed.exponent, parsed.mantissa + 1)) {
|
||||
return false;
|
||||
}
|
||||
am = fast_float::compute_error<fast_float::binary_format<double>>(
|
||||
parsed.exponent, parsed.mantissa);
|
||||
if (am.power2 >= 0) {
|
||||
return false;
|
||||
}
|
||||
target.number = parsed;
|
||||
target.am = am;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool takes_digit_comp(std::string const &input) {
|
||||
target_case target;
|
||||
return make_target_case(input, target);
|
||||
}
|
||||
|
||||
uint64_t bits(double value) {
|
||||
uint64_t result;
|
||||
::memcpy(&result, &value, sizeof(result));
|
||||
return result;
|
||||
}
|
||||
|
||||
void parse(std::string const &input, double &value, std::errc &error,
|
||||
size_t &parsed_length) {
|
||||
fast_float::from_chars_result const result = fast_float::from_chars(
|
||||
input.data(), input.data() + input.size(), value);
|
||||
error = result.ec;
|
||||
parsed_length = size_t(result.ptr - input.data());
|
||||
}
|
||||
|
||||
bool verify_case(input_case const &test) {
|
||||
double input_value = 0;
|
||||
double canonical_value = 0;
|
||||
std::errc input_error;
|
||||
std::errc canonical_error;
|
||||
size_t input_length = 0;
|
||||
size_t canonical_length = 0;
|
||||
parse(test.input, input_value, input_error, input_length);
|
||||
parse(test.canonical, canonical_value, canonical_error, canonical_length);
|
||||
return takes_digit_comp(test.input) &&
|
||||
input_length + 1 == test.input.size() &&
|
||||
canonical_length + 1 == test.canonical.size() &&
|
||||
input_error == canonical_error &&
|
||||
bits(input_value) == bits(canonical_value);
|
||||
}
|
||||
|
||||
std::vector<input_case> make_cases(
|
||||
std::vector<size_t> const &zero_counts,
|
||||
std::vector<size_t> const &core_lengths) {
|
||||
// This prefix makes compute_float(m) and compute_float(m + 1) differ at
|
||||
// exponent -18, so public from_chars reaches digit_comp after parsing a
|
||||
// coefficient longer than 19 digits.
|
||||
std::string const prefix = "6497987825129815764";
|
||||
std::vector<input_case> result;
|
||||
for (size_t core_length : core_lengths) {
|
||||
std::string const tail = nonzero_tail(core_length - prefix.size());
|
||||
for (size_t zero_count : zero_counts) {
|
||||
std::string const zeroes(zero_count, '0');
|
||||
std::string const integer_exponent =
|
||||
std::to_string(-18 - int(tail.size()) - int(zero_count));
|
||||
|
||||
// Put a non-digit marker after each number so verification also checks
|
||||
// the public from_chars pointer result.
|
||||
result.push_back(input_case{prefix + tail + zeroes + "e" +
|
||||
integer_exponent + "x",
|
||||
prefix + tail + "e" +
|
||||
std::to_string(-18 - int(tail.size())) +
|
||||
"x"});
|
||||
result.push_back(input_case{"0." + prefix + tail + zeroes + "e1x",
|
||||
"0." + prefix + tail + "e1x"});
|
||||
result.push_back(input_case{prefix.substr(0, 1) + "." +
|
||||
prefix.substr(1) + tail + zeroes + "e0x",
|
||||
prefix.substr(0, 1) + "." +
|
||||
prefix.substr(1) + tail + "e0x"});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<input_case> make_cases(std::vector<size_t> const &zero_counts) {
|
||||
return make_cases(zero_counts, {size_t(20), size_t(30), size_t(120)});
|
||||
}
|
||||
|
||||
bool verify(std::vector<input_case> const &cases) {
|
||||
for (input_case const &test : cases) {
|
||||
if (!verify_case(test)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<input_case> make_ordinary_cases() {
|
||||
std::vector<input_case> result;
|
||||
result.reserve(1000);
|
||||
for (size_t index = 0; index < 1000; ++index) {
|
||||
int const integer = int(index % 97);
|
||||
int const fraction = int((index * 17) % 100000);
|
||||
int const exponent = int(index % 15) - 7;
|
||||
std::string input;
|
||||
switch (index % 4) {
|
||||
case 0:
|
||||
input = std::to_string(integer) + "." + std::to_string(fraction);
|
||||
break;
|
||||
case 1:
|
||||
input = std::to_string(integer) + "." + std::to_string(fraction) +
|
||||
"e" + std::to_string(exponent);
|
||||
break;
|
||||
case 2:
|
||||
input = "-" + std::to_string(integer) + "." +
|
||||
std::to_string(fraction) + "e" + std::to_string(exponent);
|
||||
break;
|
||||
default:
|
||||
input = "0." + std::to_string(fraction) + "e" +
|
||||
std::to_string(exponent);
|
||||
break;
|
||||
}
|
||||
result.push_back(input_case{input + "x", std::string()});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool verify_ordinary(std::vector<input_case> const &cases) {
|
||||
for (input_case const &test : cases) {
|
||||
double value = 0;
|
||||
std::errc error;
|
||||
size_t parsed_length = 0;
|
||||
parse(test.input, value, error, parsed_length);
|
||||
if (error != std::errc() || parsed_length + 1 != test.input.size() ||
|
||||
takes_digit_comp(test.input)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void parse_all(std::vector<input_case> const &cases, size_t iterations) {
|
||||
uint64_t local_sink = 0;
|
||||
for (size_t iteration = 0; iteration < iterations; ++iteration) {
|
||||
for (input_case const &test : cases) {
|
||||
double value = 0;
|
||||
std::errc error;
|
||||
size_t parsed_length = 0;
|
||||
parse(test.input, value, error, parsed_length);
|
||||
local_sink += bits(value) + uint64_t(parsed_length) + uint64_t(error);
|
||||
}
|
||||
}
|
||||
sink += local_sink;
|
||||
}
|
||||
|
||||
double benchmark(std::vector<input_case> const &cases, size_t iterations) {
|
||||
// Keep input construction and correctness validation out of the measured
|
||||
// parse operation, as callers normally own the input buffers already.
|
||||
parse_all(cases, 1);
|
||||
std::chrono::steady_clock::time_point const start =
|
||||
std::chrono::steady_clock::now();
|
||||
parse_all(cases, iterations);
|
||||
std::chrono::steady_clock::duration const elapsed =
|
||||
std::chrono::steady_clock::now() - start;
|
||||
double const operations = double(cases.size()) * double(iterations);
|
||||
return std::chrono::duration<double, std::nano>(elapsed).count() /
|
||||
operations;
|
||||
}
|
||||
|
||||
double benchmark(std::vector<input_case> const &cases) {
|
||||
return benchmark(cases, 2000);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc != 2) {
|
||||
std::cerr << "usage: trailing_zero_benchmark "
|
||||
"--verify|--boundary-benchmark|--pre-cutoff-benchmark|"
|
||||
"--medium-suffix-benchmark|--ordinary-benchmark|--benchmark\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
std::string const mode(argv[1]);
|
||||
if (mode == "--verify") {
|
||||
std::vector<input_case> const cases = make_cases(
|
||||
{size_t(0), size_t(1), size_t(8), size_t(15), size_t(16), size_t(17),
|
||||
size_t(32), size_t(63), size_t(64), size_t(65), size_t(700),
|
||||
size_t(769), size_t(1000), size_t(4096)},
|
||||
{size_t(20), size_t(30), size_t(120), size_t(720), size_t(769)});
|
||||
if (!verify(cases)) {
|
||||
std::cerr << "trailing-zero verification failed\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
if (mode == "--boundary-benchmark") {
|
||||
std::vector<size_t> zero_counts;
|
||||
for (size_t zero_count = 0; zero_count <= 65; ++zero_count) {
|
||||
zero_counts.push_back(zero_count);
|
||||
}
|
||||
std::vector<input_case> const cases = make_cases(
|
||||
zero_counts, {size_t(20), size_t(120), size_t(720), size_t(769)});
|
||||
if (!verify(cases)) {
|
||||
std::cerr << "trailing-zero verification failed\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
std::cout << "{\"metric\":\"ns_per_boundary_parse\",\"value\":"
|
||||
<< std::fixed << std::setprecision(3) << benchmark(cases, 1000)
|
||||
<< "}\n";
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
if (mode == "--pre-cutoff-benchmark") {
|
||||
std::vector<size_t> zero_counts;
|
||||
for (size_t zero_count = 0; zero_count < 16; ++zero_count) {
|
||||
zero_counts.push_back(zero_count);
|
||||
}
|
||||
std::vector<input_case> const cases = make_cases(
|
||||
zero_counts,
|
||||
{size_t(20), size_t(30), size_t(120), size_t(720), size_t(769)});
|
||||
if (!verify(cases)) {
|
||||
std::cerr << "trailing-zero verification failed\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
std::cout << "{\"metric\":\"ns_per_pre_cutoff_parse\",\"value\":"
|
||||
<< std::fixed << std::setprecision(3) << benchmark(cases, 1000)
|
||||
<< "}\n";
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
if (mode == "--medium-suffix-benchmark") {
|
||||
std::vector<size_t> zero_counts;
|
||||
for (size_t zero_count = 16; zero_count < 64; ++zero_count) {
|
||||
zero_counts.push_back(zero_count);
|
||||
}
|
||||
std::vector<input_case> const cases = make_cases(
|
||||
zero_counts, {size_t(120), size_t(720), size_t(769)});
|
||||
if (!verify(cases)) {
|
||||
std::cerr << "trailing-zero verification failed\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
std::cout << "{\"metric\":\"ns_per_medium_suffix_parse\",\"value\":"
|
||||
<< std::fixed << std::setprecision(3) << benchmark(cases, 1000)
|
||||
<< "}\n";
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
if (mode == "--ordinary-benchmark") {
|
||||
std::vector<input_case> const cases = make_ordinary_cases();
|
||||
if (!verify_ordinary(cases)) {
|
||||
std::cerr << "ordinary verification failed\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
std::cout << "{\"metric\":\"ns_per_ordinary_parse\",\"value\":"
|
||||
<< std::fixed << std::setprecision(3) << benchmark(cases, 10000)
|
||||
<< "}\n";
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
if (mode == "--benchmark") {
|
||||
std::vector<input_case> const cases = make_cases(
|
||||
{size_t(64), size_t(700), size_t(769), size_t(4096)});
|
||||
if (!verify(cases)) {
|
||||
std::cerr << "trailing-zero verification failed\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
std::cout << "{\"metric\":\"ns_per_parse\",\"value\":"
|
||||
<< std::fixed << std::setprecision(3) << benchmark(cases)
|
||||
<< "}\n";
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
std::cerr << "unknown mode: " << mode << '\n';
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
@ -445,6 +445,123 @@ digit_comp(parsed_number_string_t<UC> &num, adjusted_mantissa am) noexcept {
|
||||
}
|
||||
}
|
||||
|
||||
// A fixed-width confirmation is cheaper than rebuilding the corresponding
|
||||
// bigint limbs once a coefficient has this many terminal zeroes.
|
||||
constexpr size_t minimum_trailing_zeroes = 16;
|
||||
|
||||
template <typename UC>
|
||||
fastfloat_really_inline FASTFLOAT_CONSTEXPR20 bool
|
||||
trim_at_least_zeros_from_end(UC const *first, UC const *&last,
|
||||
size_t zeroes) noexcept {
|
||||
if (size_t(last - first) < zeroes) {
|
||||
return false;
|
||||
}
|
||||
while (!cpp20_and_in_constexpr() &&
|
||||
zeroes >= size_t(int_cmp_len<UC>())) {
|
||||
uint64_t value;
|
||||
last -= int_cmp_len<UC>();
|
||||
::memcpy(&value, last, sizeof(value));
|
||||
if (value != int_cmp_zeros<UC>()) {
|
||||
return false;
|
||||
}
|
||||
zeroes -= size_t(int_cmp_len<UC>());
|
||||
}
|
||||
while (zeroes != 0) {
|
||||
--last;
|
||||
if (*last != UC('0')) {
|
||||
return false;
|
||||
}
|
||||
--zeroes;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename UC>
|
||||
fastfloat_really_inline FASTFLOAT_CONSTEXPR20 void
|
||||
trim_zeros_from_end(UC const *first, UC const *&last) noexcept {
|
||||
while (!cpp20_and_in_constexpr() &&
|
||||
std::distance(first, last) >= int_cmp_len<UC>()) {
|
||||
uint64_t value;
|
||||
::memcpy(&value, last - int_cmp_len<UC>(), sizeof(value));
|
||||
if (value != int_cmp_zeros<UC>()) {
|
||||
break;
|
||||
}
|
||||
last -= int_cmp_len<UC>();
|
||||
}
|
||||
while (last != first && last[-1] == UC('0')) {
|
||||
--last;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename UC>
|
||||
fastfloat_really_inline FASTFLOAT_CONSTEXPR20 bool
|
||||
ends_in_sixteen_zeroes(UC const *first, UC const *last) noexcept {
|
||||
if (size_t(last - first) < minimum_trailing_zeroes) {
|
||||
return false;
|
||||
}
|
||||
if (!cpp20_and_in_constexpr() && sizeof(UC) == 1) {
|
||||
return ::memcmp(last - minimum_trailing_zeroes,
|
||||
"0000000000000000", minimum_trailing_zeroes) == 0;
|
||||
}
|
||||
return trim_at_least_zeros_from_end(first, last,
|
||||
minimum_trailing_zeroes);
|
||||
}
|
||||
|
||||
template <typename UC>
|
||||
fastfloat_really_inline FASTFLOAT_CONSTEXPR20 bool
|
||||
has_minimum_trailing_zeroes(parsed_number_string_t<UC> const &num) noexcept {
|
||||
if (num.fraction.ptr == nullptr) {
|
||||
return ends_in_sixteen_zeroes(num.integer.ptr,
|
||||
num.integer.ptr + num.integer.len());
|
||||
}
|
||||
UC const *fraction_end = num.fraction.ptr + num.fraction.len();
|
||||
if (num.fraction.len() >= minimum_trailing_zeroes) {
|
||||
return ends_in_sixteen_zeroes(num.fraction.ptr, fraction_end);
|
||||
}
|
||||
if (!trim_at_least_zeros_from_end(num.fraction.ptr, fraction_end,
|
||||
num.fraction.len())) {
|
||||
return false;
|
||||
}
|
||||
UC const *integer_end = num.integer.ptr + num.integer.len();
|
||||
return trim_at_least_zeros_from_end(
|
||||
num.integer.ptr, integer_end,
|
||||
minimum_trailing_zeroes - num.fraction.len());
|
||||
}
|
||||
|
||||
template <typename UC>
|
||||
fastfloat_really_inline FASTFLOAT_CONSTEXPR20 bool
|
||||
trim_trailing_zeros(parsed_number_string_t<UC> const &num,
|
||||
parsed_number_string_t<UC> &trimmed) noexcept {
|
||||
UC const *integer_end = num.integer.ptr + num.integer.len();
|
||||
UC const *fraction_end = num.fraction.ptr;
|
||||
if (fraction_end != nullptr) {
|
||||
fraction_end += num.fraction.len();
|
||||
trim_zeros_from_end(num.fraction.ptr, fraction_end);
|
||||
}
|
||||
if (fraction_end == nullptr || fraction_end == num.fraction.ptr) {
|
||||
trim_zeros_from_end(num.integer.ptr, integer_end);
|
||||
}
|
||||
if (integer_end == num.integer.ptr &&
|
||||
(fraction_end == nullptr || fraction_end == num.fraction.ptr)) {
|
||||
return false;
|
||||
}
|
||||
trimmed = num;
|
||||
trimmed.integer =
|
||||
span<UC const>(num.integer.ptr, size_t(integer_end - num.integer.ptr));
|
||||
if (fraction_end != nullptr) {
|
||||
trimmed.fraction = span<UC const>(
|
||||
num.fraction.ptr, size_t(fraction_end - num.fraction.ptr));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename UC>
|
||||
static fastfloat_noinline FASTFLOAT_CONSTEXPR20 bool
|
||||
trim_confirmed_trailing_zeroes(parsed_number_string_t<UC> const &num,
|
||||
parsed_number_string_t<UC> &trimmed) noexcept {
|
||||
return trim_trailing_zeros(num, trimmed);
|
||||
}
|
||||
|
||||
} // namespace fast_float
|
||||
|
||||
#endif
|
||||
|
||||
@ -213,8 +213,13 @@ using parse_options = parse_options_t<char>;
|
||||
|
||||
#ifdef FASTFLOAT_VISUAL_STUDIO
|
||||
#define fastfloat_really_inline __forceinline
|
||||
#else
|
||||
#define fastfloat_noinline __declspec(noinline)
|
||||
#elif defined(__GNUC__) || defined(__clang__)
|
||||
#define fastfloat_really_inline inline __attribute__((always_inline))
|
||||
#define fastfloat_noinline __attribute__((noinline))
|
||||
#else
|
||||
#define fastfloat_really_inline inline
|
||||
#define fastfloat_noinline
|
||||
#endif
|
||||
|
||||
// Opposite of fastfloat_really_inline. Used for a rare, format-specific variant
|
||||
|
||||
@ -315,7 +315,16 @@ from_chars_advanced(parsed_number_string_t<UC> &pns, T &value) noexcept {
|
||||
// and we have an invalid power (am.power2 < 0), then we need to go the long
|
||||
// way around again. This is very uncommon.
|
||||
if (am.power2 < 0) {
|
||||
am = digit_comp<T>(pns, am);
|
||||
if fastfloat_unlikely(has_minimum_trailing_zeroes(pns)) {
|
||||
parsed_number_string_t<UC> trimmed;
|
||||
if (trim_confirmed_trailing_zeroes(pns, trimmed)) {
|
||||
am = digit_comp<T>(trimmed, am);
|
||||
} else {
|
||||
am = digit_comp<T>(pns, am);
|
||||
}
|
||||
} else {
|
||||
am = digit_comp<T>(pns, am);
|
||||
}
|
||||
}
|
||||
to_float(pns.negative, am, value);
|
||||
// Test for over/underflow.
|
||||
@ -327,14 +336,13 @@ from_chars_advanced(parsed_number_string_t<UC> &pns, T &value) noexcept {
|
||||
}
|
||||
|
||||
// Slow path: re-parse materializing the integer/fraction spans the hot no-span
|
||||
// parse skipped, then run the full algorithm. The two callers reach it only
|
||||
// through a fastfloat_unlikely branch, so the optimizer keeps this re-parse off
|
||||
// the hot path on its own (no function-level noinline needed).
|
||||
// from_chars_advanced already handles both the too_many_digits disambiguation
|
||||
// and the am.power2<0 digit_comp recompute, so both slow branches collapse to
|
||||
// one helper call.
|
||||
// parse skipped, then run the full algorithm. Keep it out of line so the rare
|
||||
// reparse and suffix probe do not inflate the common parser.
|
||||
// from_chars_advanced handles both the too_many_digits disambiguation and the
|
||||
// am.power2<0 digit_comp recompute, so both slow branches collapse to one
|
||||
// helper call.
|
||||
template <typename T, typename UC>
|
||||
FASTFLOAT_CONSTEXPR20 from_chars_result_t<UC>
|
||||
static fastfloat_noinline FASTFLOAT_CONSTEXPR20 from_chars_result_t<UC>
|
||||
parse_number_slow_path(UC const *first, UC const *last, T &value,
|
||||
parse_options_t<UC> options, bool bjf) noexcept {
|
||||
parsed_number_string_t<UC> pns =
|
||||
|
||||
@ -79,6 +79,15 @@ cc_test(
|
||||
],
|
||||
)
|
||||
|
||||
cc_test(
|
||||
name = "trailing_zeros_test",
|
||||
srcs = ["trailing_zeros_test.cpp"],
|
||||
deps = [
|
||||
"//:fast_float",
|
||||
"@doctest//doctest",
|
||||
],
|
||||
)
|
||||
|
||||
cc_test(
|
||||
name = "powersoffive_hardround",
|
||||
srcs = ["powersoffive_hardround.cpp"],
|
||||
|
||||
@ -80,6 +80,7 @@ if (FASTFLOAT_SUPPLEMENTAL_TESTS)
|
||||
endif()
|
||||
fast_float_add_cpp_test(p2497)
|
||||
fast_float_add_cpp_test(long_test)
|
||||
fast_float_add_cpp_test(trailing_zeros_test)
|
||||
fast_float_add_cpp_test(powersoffive_hardround)
|
||||
fast_float_add_cpp_test(string_test)
|
||||
fast_float_add_cpp_test(fast_int)
|
||||
|
||||
231
tests/trailing_zeros_test.cpp
Normal file
231
tests/trailing_zeros_test.cpp
Normal file
@ -0,0 +1,231 @@
|
||||
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
|
||||
#include "doctest/doctest.h"
|
||||
|
||||
#include "fast_float/fast_float.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
struct parse_result {
|
||||
uint64_t value_bits;
|
||||
size_t parsed_length;
|
||||
std::errc error;
|
||||
};
|
||||
|
||||
std::string nonzero_tail(size_t length) {
|
||||
std::string result(length, '1');
|
||||
result.front() = '7';
|
||||
result.back() = '7';
|
||||
return result;
|
||||
}
|
||||
|
||||
uint64_t bits(double value) {
|
||||
uint64_t result;
|
||||
::memcpy(&result, &value, sizeof(result));
|
||||
return result;
|
||||
}
|
||||
|
||||
parse_result parse(std::string const &input) {
|
||||
double value = 0;
|
||||
fast_float::from_chars_result const result = fast_float::from_chars(
|
||||
input.data(), input.data() + input.size(), value);
|
||||
return parse_result{bits(value), size_t(result.ptr - input.data()), result.ec};
|
||||
}
|
||||
|
||||
fast_float::parsed_number_string parse_number(std::string const &input) {
|
||||
fast_float::parse_options options;
|
||||
return fast_float::parse_number_string<false, char>(
|
||||
input.data(), input.data() + input.size(), options, true);
|
||||
}
|
||||
|
||||
bool takes_digit_comp(std::string const &input) {
|
||||
fast_float::parse_options options;
|
||||
fast_float::parsed_number_string const parsed =
|
||||
fast_float::parse_number_string<false, char>(
|
||||
input.data(), input.data() + input.size(), options, true);
|
||||
fast_float::adjusted_mantissa am =
|
||||
fast_float::compute_float<fast_float::binary_format<double>>(
|
||||
parsed.exponent, parsed.mantissa);
|
||||
if (parsed.too_many_digits && am.power2 >= 0 &&
|
||||
am != fast_float::compute_float<fast_float::binary_format<double>>(
|
||||
parsed.exponent, parsed.mantissa + 1)) {
|
||||
am = fast_float::compute_error<fast_float::binary_format<double>>(
|
||||
parsed.exponent, parsed.mantissa);
|
||||
}
|
||||
return parsed.valid && am.power2 < 0;
|
||||
}
|
||||
|
||||
void check_equivalent(std::string const &padded, std::string const &canonical) {
|
||||
std::string const padded_with_marker = padded + "x";
|
||||
std::string const canonical_with_marker = canonical + "x";
|
||||
REQUIRE(takes_digit_comp(padded_with_marker));
|
||||
|
||||
parse_result const padded_result = parse(padded_with_marker);
|
||||
parse_result const canonical_result = parse(canonical_with_marker);
|
||||
CHECK(padded_result.parsed_length == padded.size());
|
||||
CHECK(canonical_result.parsed_length == canonical.size());
|
||||
CHECK(padded_result.error == canonical_result.error);
|
||||
CHECK(padded_result.value_bits == canonical_result.value_bits);
|
||||
}
|
||||
|
||||
template <typename UC> std::basic_string<UC> widen(std::string const &input) {
|
||||
return std::basic_string<UC>(input.begin(), input.end());
|
||||
}
|
||||
|
||||
template <typename UC> parse_result parse_wide(std::string const &input) {
|
||||
std::basic_string<UC> const wide = widen<UC>(input);
|
||||
double value = 0;
|
||||
fast_float::from_chars_result_t<UC> const result = fast_float::from_chars(
|
||||
wide.data(), wide.data() + wide.size(), value);
|
||||
return parse_result{bits(value), size_t(result.ptr - wide.data()), result.ec};
|
||||
}
|
||||
|
||||
template <typename UC>
|
||||
void check_equivalent_wide(std::string const &padded,
|
||||
std::string const &canonical) {
|
||||
std::string const padded_with_marker = padded + "x";
|
||||
std::string const canonical_with_marker = canonical + "x";
|
||||
parse_result const padded_result = parse_wide<UC>(padded_with_marker);
|
||||
parse_result const canonical_result = parse_wide<UC>(canonical_with_marker);
|
||||
CHECK(padded_result.parsed_length == padded.size());
|
||||
CHECK(canonical_result.parsed_length == canonical.size());
|
||||
CHECK(padded_result.error == canonical_result.error);
|
||||
CHECK(padded_result.value_bits == canonical_result.value_bits);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("long trailing zero coefficients preserve public from_chars results") {
|
||||
// The prefix is an ambiguous 19-digit mantissa at exponent -18. Adding a
|
||||
// twentieth digit makes public from_chars use the digit comparison fallback.
|
||||
std::string const prefix = "6497987825129815764";
|
||||
std::vector<size_t> const zero_counts = {
|
||||
0, 1, 8, 15, 16, 17, 32, 63, 64, 65, 700, 769, 1000, 4096};
|
||||
for (size_t core_length :
|
||||
{size_t(20), size_t(30), size_t(120), size_t(720), size_t(769)}) {
|
||||
std::string const tail = nonzero_tail(core_length - prefix.size());
|
||||
for (size_t zero_count : zero_counts) {
|
||||
std::string const zeroes(zero_count, '0');
|
||||
std::string const integer_exponent =
|
||||
std::to_string(-18 - int(tail.size()) - int(zero_count));
|
||||
|
||||
// Integer suffixes need a compensating explicit exponent. Fractional
|
||||
// suffixes do not, because the decimal point already fixes their scale.
|
||||
check_equivalent(prefix + tail + zeroes + "e" + integer_exponent,
|
||||
prefix + tail + "e" +
|
||||
std::to_string(-18 - int(tail.size())));
|
||||
check_equivalent("-" + prefix + tail + zeroes + "e" + integer_exponent,
|
||||
"-" + prefix + tail + "e" +
|
||||
std::to_string(-18 - int(tail.size())));
|
||||
check_equivalent("0." + prefix + tail + zeroes + "e1",
|
||||
"0." + prefix + tail + "e1");
|
||||
check_equivalent(prefix.substr(0, 1) + "." + prefix.substr(1) + tail +
|
||||
zeroes + "e0",
|
||||
prefix.substr(0, 1) + "." + prefix.substr(1) + tail +
|
||||
"e0");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("trailing zero fallback accepts inputs without explicit exponents") {
|
||||
// This prefix is ambiguous at exponent -19, which is the corrected exponent
|
||||
// for a fraction-only input without an explicit exponent.
|
||||
std::string const prefix = "2686910556586236953";
|
||||
for (size_t zero_count :
|
||||
{size_t(0), size_t(15), size_t(16), size_t(63), size_t(64),
|
||||
size_t(65), size_t(700), size_t(4096)}) {
|
||||
std::string const zeroes(zero_count, '0');
|
||||
check_equivalent("0." + prefix + "7" + zeroes,
|
||||
"0." + prefix + "7");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("trailing zero fallback supports wide character input") {
|
||||
std::string const prefix = "6497987825129815764";
|
||||
std::string const tail = nonzero_tail(101);
|
||||
std::string const zeroes(769, '0');
|
||||
std::string const padded = "0." + prefix + tail + zeroes + "e1";
|
||||
std::string const canonical = "0." + prefix + tail + "e1";
|
||||
check_equivalent_wide<char16_t>(padded, canonical);
|
||||
check_equivalent_wide<wchar_t>(padded, canonical);
|
||||
check_equivalent_wide<char32_t>(padded, canonical);
|
||||
}
|
||||
|
||||
TEST_CASE("all-zero coefficients retain their public result") {
|
||||
for (size_t zero_count : {size_t(0), size_t(16), size_t(700), size_t(4096)}) {
|
||||
std::string const integer = "0" + std::string(zero_count, '0') + "e10x";
|
||||
std::string const fraction = "0." + std::string(zero_count, '0') + "e-10x";
|
||||
parse_result const integer_result = parse(integer);
|
||||
parse_result const fraction_result = parse(fraction);
|
||||
CHECK(integer_result.parsed_length + 1 == integer.size());
|
||||
CHECK(fraction_result.parsed_length + 1 == fraction.size());
|
||||
CHECK(integer_result.error == std::errc());
|
||||
CHECK(fraction_result.error == std::errc());
|
||||
CHECK(integer_result.value_bits == 0);
|
||||
CHECK(fraction_result.value_bits == 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("trailing zero normalization shortens only logical spans") {
|
||||
std::string const zeroes64(64, '0');
|
||||
|
||||
std::string const fraction_input = "120.3" + zeroes64;
|
||||
fast_float::parsed_number_string fraction = parse_number(fraction_input);
|
||||
fast_float::parsed_number_string trimmed;
|
||||
REQUIRE(fast_float::has_minimum_trailing_zeroes(fraction));
|
||||
REQUIRE(fast_float::trim_trailing_zeros(fraction, trimmed));
|
||||
CHECK(fraction.integer.len() == 3);
|
||||
CHECK(fraction.fraction.len() == 65);
|
||||
CHECK(trimmed.integer.len() == 3);
|
||||
CHECK(trimmed.fraction.len() == 1);
|
||||
CHECK(std::string(trimmed.integer.ptr, trimmed.integer.len()) == "120");
|
||||
CHECK(std::string(trimmed.fraction.ptr, trimmed.fraction.len()) == "3");
|
||||
|
||||
std::string const across_point_input = "120." + zeroes64;
|
||||
fast_float::parsed_number_string across_point =
|
||||
parse_number(across_point_input);
|
||||
REQUIRE(fast_float::has_minimum_trailing_zeroes(across_point));
|
||||
REQUIRE(fast_float::trim_trailing_zeros(across_point, trimmed));
|
||||
CHECK(trimmed.integer.len() == 2);
|
||||
CHECK(trimmed.fraction.len() == 0);
|
||||
CHECK(std::string(trimmed.integer.ptr, trimmed.integer.len()) == "12");
|
||||
|
||||
std::string const integer_input = "123" + zeroes64;
|
||||
fast_float::parsed_number_string integer_only = parse_number(integer_input);
|
||||
REQUIRE(fast_float::has_minimum_trailing_zeroes(integer_only));
|
||||
REQUIRE(fast_float::trim_trailing_zeros(integer_only, trimmed));
|
||||
CHECK(trimmed.integer.len() == 3);
|
||||
CHECK(trimmed.fraction.len() == 0);
|
||||
CHECK(std::string(trimmed.integer.ptr, trimmed.integer.len()) == "123");
|
||||
|
||||
std::string const short_suffix_input = "120.3" + std::string(15, '0');
|
||||
fast_float::parsed_number_string short_suffix =
|
||||
parse_number(short_suffix_input);
|
||||
CHECK_FALSE(fast_float::has_minimum_trailing_zeroes(short_suffix));
|
||||
|
||||
std::string const cutoff_input = "120.3" + std::string(16, '0');
|
||||
fast_float::parsed_number_string cutoff = parse_number(cutoff_input);
|
||||
REQUIRE(fast_float::has_minimum_trailing_zeroes(cutoff));
|
||||
REQUIRE(fast_float::trim_trailing_zeros(cutoff, trimmed));
|
||||
CHECK(trimmed.integer.len() == 3);
|
||||
CHECK(trimmed.fraction.len() == 1);
|
||||
|
||||
std::string const cross_span_input =
|
||||
"123" + std::string(15, '0') + ".0";
|
||||
fast_float::parsed_number_string cross_span = parse_number(cross_span_input);
|
||||
REQUIRE(fast_float::has_minimum_trailing_zeroes(cross_span));
|
||||
REQUIRE(fast_float::trim_trailing_zeros(cross_span, trimmed));
|
||||
CHECK(trimmed.integer.len() == 3);
|
||||
CHECK(trimmed.fraction.len() == 0);
|
||||
CHECK(std::string(trimmed.integer.ptr, trimmed.integer.len()) == "123");
|
||||
|
||||
std::string const all_zero_input = "0." + zeroes64;
|
||||
fast_float::parsed_number_string all_zero = parse_number(all_zero_input);
|
||||
REQUIRE(fast_float::has_minimum_trailing_zeroes(all_zero));
|
||||
CHECK_FALSE(fast_float::trim_trailing_zeros(all_zero, trimmed));
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user