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
319 changes: 319 additions & 0 deletions benchmarks/trailing_zero_benchmark.cpp
Original file line number Diff line number Diff line change
@@ -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;
}
Loading
Loading