Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
252 changes: 252 additions & 0 deletions benchmarks/trailing_zero_benchmark.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
#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) {
// 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 : {size_t(20), size_t(30), size_t(120)}) {
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;
}

bool verify(std::vector<input_case> const &cases) {
for (input_case const &test : cases) {
if (!verify_case(test)) {
return false;
}
}
return true;
}

bool make_target_cases(std::vector<input_case> const &cases,
std::vector<target_case> &targets) {
targets.clear();
targets.reserve(cases.size());
for (input_case const &test : cases) {
target_case target;
if (!make_target_case(test.input, target)) {
return false;
}
targets.push_back(target);
}
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) {
// Keep input construction and correctness validation out of the measured
// parse operation, as callers normally own the input buffers already.
parse_all(cases, 1);
size_t const iterations = 2000;
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;
}

void digit_comp_all(std::vector<target_case> &targets, size_t iterations) {
uint64_t local_sink = 0;
for (size_t iteration = 0; iteration < iterations; ++iteration) {
for (target_case &target : targets) {
fast_float::adjusted_mantissa const answer =
fast_float::digit_comp<double>(target.number, target.am);
local_sink += answer.mantissa + uint64_t(answer.power2);
}
}
sink += local_sink;
}

double benchmark_digit_comp(std::vector<target_case> &targets) {
digit_comp_all(targets, 1);
size_t const iterations = 20000;
std::chrono::steady_clock::time_point const start =
std::chrono::steady_clock::now();
digit_comp_all(targets, iterations);
std::chrono::steady_clock::duration const elapsed =
std::chrono::steady_clock::now() - start;
double const operations = double(targets.size()) * double(iterations);
return std::chrono::duration<double, std::nano>(elapsed).count() /
operations;
}

} // namespace

int main(int argc, char **argv) {
if (argc != 2) {
std::cerr << "usage: trailing_zero_benchmark "
"--verify|--benchmark|--target-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(64), size_t(700), size_t(769), size_t(1000), size_t(4096)});
if (!verify(cases)) {
std::cerr << "trailing-zero verification failed\n";
return EXIT_FAILURE;
}
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;
}

if (mode == "--target-benchmark") {
std::vector<input_case> const cases = make_cases(
{size_t(64), size_t(700), size_t(769), size_t(4096)});
std::vector<target_case> targets;
if (!verify(cases) || !make_target_cases(cases, targets)) {
std::cerr << "trailing-zero verification failed\n";
return EXIT_FAILURE;
}
std::cout << "{\"metric\":\"ns_per_digit_comp\",\"value\":"
<< std::fixed << std::setprecision(3)
<< benchmark_digit_comp(targets) << "}\n";
return EXIT_SUCCESS;
}

std::cerr << "unknown mode: " << mode << '\n';
return EXIT_FAILURE;
}
86 changes: 85 additions & 1 deletion include/fast_float/digit_comparison.h
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,82 @@ inline FASTFLOAT_CONSTEXPR20 adjusted_mantissa negative_digit_comp(
// `b` as a big-integer type, scaled to the same binary exponent as
// the actual digits. we then compare the big integer representations
// of both, and use that to direct rounding.
template <typename UC>
fastfloat_really_inline FASTFLOAT_CONSTEXPR20 bool
has_trailing_zero(parsed_number_string_t<UC> const &num) noexcept {
if (num.fraction.ptr != nullptr && num.fraction.len() != 0) {
return num.fraction[num.fraction.len() - 1] == UC('0');
}
return num.integer.len() != 0 &&
num.integer[num.integer.len() - 1] == UC('0');
}

// The long decimal fallback is only reached for an ambiguous conversion. For
// a sufficiently long zero suffix, scanning backward is cheaper than building
// zero limbs in the bigint. This helper leaves `last` at the final nonzero
// digit and returns the number of zeroes it removed.
template <typename UC>
fastfloat_really_inline FASTFLOAT_CONSTEXPR20 size_t
trim_zeros_from_end(UC const *first, UC const *&last) noexcept {
size_t zeroes = 0;
while (!cpp20_and_in_constexpr() &&
std::distance(first, last) >= int_cmp_len<UC>()) {
uint64_t value;
::memcpy(&value, last - int_cmp_len<UC>(), sizeof(uint64_t));
if (value != int_cmp_zeros<UC>()) {
break;
}
last -= int_cmp_len<UC>();
zeroes += size_t(int_cmp_len<UC>());
}
while (last != first && last[-1] == UC('0')) {
--last;
++zeroes;
}
return zeroes;
}

// Discard a long suffix of zeroes before materializing the coefficient. The
// scientific exponent still comes from the original parsed number, so removing
// zeroes here is balanced by the scale derived from the shorter digit count.
template <typename UC>
fastfloat_really_inline FASTFLOAT_CONSTEXPR20 bool
trim_trailing_zeros(parsed_number_string_t<UC> &num) noexcept {
constexpr size_t minimum_trailing_zeroes = 16;
if (!has_trailing_zero(num)) {
return false;
}

UC const *integer_end = num.integer.ptr + num.integer.len();
UC const *fraction_end = num.fraction.ptr;
size_t trailing_zeroes = 0;
if (fraction_end != nullptr) {
fraction_end += num.fraction.len();
trailing_zeroes = trim_zeros_from_end(num.fraction.ptr, fraction_end);
}
// Integer zeroes belong to the suffix only when every fractional digit was
// zero. For example, trimming 120.3000 must retain the zero in 120.
if (fraction_end == nullptr || fraction_end == num.fraction.ptr) {
trailing_zeroes += trim_zeros_from_end(num.integer.ptr, integer_end);
}

// Do not turn an all-zero coefficient into empty spans, and retain short
// suffixes where a reverse scan does not recover its setup cost.
if (trailing_zeroes < minimum_trailing_zeroes ||
(integer_end == num.integer.ptr &&
(fraction_end == nullptr || fraction_end == num.fraction.ptr))) {
return false;
}

num.integer = span<UC const>(
num.integer.ptr, size_t(integer_end - num.integer.ptr));
if (fraction_end != nullptr) {
num.fraction = span<UC const>(
num.fraction.ptr, size_t(fraction_end - num.fraction.ptr));
}
return true;
}

template <typename T, typename UC>
inline FASTFLOAT_CONSTEXPR20 adjusted_mantissa
digit_comp(parsed_number_string_t<UC> &num, adjusted_mantissa am) noexcept {
Expand All @@ -439,7 +515,15 @@ digit_comp(parsed_number_string_t<UC> &num, adjusted_mantissa am) noexcept {
size_t max_digits = binary_format<T>::max_digits();
size_t digits = 0;
bigint bigmant;
parse_mantissa(bigmant, num, max_digits, digits);
parsed_number_string_t<UC> trimmed_num;
parsed_number_string_t<UC> *mantissa_num = &num;
if (has_trailing_zero(num)) {
trimmed_num = num;
if (trim_trailing_zeros(trimmed_num)) {
mantissa_num = &trimmed_num;
}
}
parse_mantissa(bigmant, *mantissa_num, max_digits, digits);
// can't underflow, since digits is at most max_digits.
int32_t exponent = sci_exp + 1 - int32_t(digits);
if (exponent >= 0) {
Expand Down
9 changes: 9 additions & 0 deletions tests/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,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"],
Expand Down
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading