Skip to content
Merged
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
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,19 @@ cmake --build build --config Release
./build/benchmarks/thoroughfloat64
```

## Fixed-precision evaluation

By default, we compare algorithms that output shortest-significand representation
which can round-trip to the original floating-point value. We can also compare
algorithms that output fixed-precision representation of a given precision:

```
./build/benchmarks/benchmark -f data/canada.txt -F [precision]
```

Note that this only works when we are comparing speeds, not measuring properties
of the algorithms, i.e., we can't use both `-F/--fixed` and `-t/--test` at the same time.

## Other existing benchmarks

- [dtoa Benchmark](https://github.com/miloyip/dtoa-benchmark)
Expand Down
268 changes: 177 additions & 91 deletions benchmarks/algorithms.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,44 +44,31 @@
#define YY_DOUBLE_SUPPORTED 0
#endif

namespace Benchmarks {

enum Algorithm {
DRAGON4 = 0,
ERROL3 = 1,
TO_STRING = 2,
FMT_FORMAT = 3,
NETLIB = 4,
SNPRINTF = 5,
GRISU2 = 6,
GRISU_EXACT = 7,
SCHUBFACH = 8,
DRAGONBOX = 9,
RYU = 10,
TEJU_JAGUA = 11,
DOUBLE_CONVERSION = 12,
ABSEIL = 13,
STD_TO_CHARS = 14,
GRISU3 = 15,
SWIFT_DTOA = 16,
YY_DOUBLE = 17,
COUNT // Keep last
};

template<arithmetic_float T>
struct BenchArgs {
using Type = T;
using BenchFn = std::function<int(T, std::span<char>&)>;

BenchArgs(const std::string& name = {}, int (*func)(T, std::span<char>&) = {},
bool used = true, size_t testRepeat = 100)
BenchArgs(const std::string& name = {}, BenchFn func = {}, bool used = true, size_t testRepeat = 100)
: name(name), func(func), used(used), testRepeat(testRepeat) {}

std::string name{};
int (*func)(T, std::span<char>&){};
BenchFn func{};
bool used{};
size_t testRepeat{100};

static void initFixedSize(size_t size) {
fixedSize = size;
snprintf(formatStr, sizeof(formatStr), "%%.%zug", fixedSize);
formatStrStr = fmt::format("{{:.{}g}}", fixedSize);
}
static inline size_t fixedSize;
static inline char formatStr[10];
static inline std::string formatStrStr;
};

namespace BenchmarkShortest {

template<arithmetic_float T>
int dragon4(T d, std::span<char>& buffer) {
if constexpr (std::is_same_v<T, float>)
Expand All @@ -92,32 +79,6 @@ int dragon4(T d, std::span<char>& buffer) {
PrintFloatFormat_Positional, -1);
}

// No errol3 implementation optimized for float instead of double ?
template<arithmetic_float T>
int errol3(T d, std::span<char>& buffer) {
#if ERROL_SUPPORTED
errol3_dtoa(d, buffer.data()); // returns the exponent
return std::strlen(buffer.data());
#else
std::cerr << "errol3 not supported" << std::endl;
std::abort();
#endif
}

template<arithmetic_float T>
int to_string(T d, std::span<char>& buffer) {
const std::string s = std::to_string(d);
std::copy(s.begin(), s.end(), buffer.begin());
return s.size();
}

template<arithmetic_float T>
int fmt_format(T d, std::span<char>& buffer) {
const std::string s = fmt::format("{}", d);
std::copy(s.begin(), s.end(), buffer.begin());
return s.size();
}

// There's no "ftoa", only "dtoa", so not optimized for float.
template<arithmetic_float T>
int netlib(T d, std::span<char>& buffer) {
Expand Down Expand Up @@ -157,7 +118,7 @@ int netlib(T d, std::span<char>& buffer) {
} else {
buffer[i++] = '0' + value;
}
};
};
// Fractional part (if any remaining digits)
const int remaining_digits = rve - (result + std::max(0, decpt));
if (remaining_digits > 0) {
Expand Down Expand Up @@ -187,12 +148,29 @@ int netlib(T d, std::span<char>& buffer) {
#endif
}

// No errol3 implementation optimized for float instead of double ?
template<arithmetic_float T>
int errol3(T d, std::span<char>& buffer) {
#if ERROL_SUPPORTED
errol3_dtoa(d, buffer.data()); // returns the exponent
return std::strlen(buffer.data());
#else
std::cerr << "errol3 not supported" << std::endl;
std::abort();
#endif
}

template<arithmetic_float T>
int snprintf(T d, std::span<char>& buffer) {
if constexpr (std::is_same_v<T, float>)
return std::snprintf(buffer.data(), buffer.size(), "%.9g", d);
else
return std::snprintf(buffer.data(), buffer.size(), "%.17g", d);
int to_string(T d, std::span<char>& buffer) {
const std::string s = std::to_string(d);
std::copy(s.begin(), s.end(), buffer.begin());
return s.size();
}

template<arithmetic_float T>
int fmt_format(T d, std::span<char>& buffer) {
const auto it = fmt::format_to(buffer.data(), "{}", d);
return std::distance(buffer.data(), it);
}

// grisu2 is hardcoded for double.
Expand Down Expand Up @@ -246,13 +224,15 @@ int teju_jagua(T d, std::span<char>& buffer) {

template<arithmetic_float T>
int double_conversion(T d, std::span<char>& buffer) {
const static double_conversion::DoubleToStringConverter converter(
double_conversion::DoubleToStringConverter::NO_FLAGS, "inf", "nan", 'e',
-4, 6, 0, 0);
using namespace double_conversion;
const static DoubleToStringConverter conv(
DoubleToStringConverter::EMIT_POSITIVE_EXPONENT_SIGN | DoubleToStringConverter::UNIQUE_ZERO,
"inf", "nan", 'e', -4, 6, 0, 0);

double_conversion::StringBuilder builder(buffer.data(), buffer.size());
const bool valid = std::is_same_v<T, float>
? converter.ToShortestSingle(d, &builder)
: converter.ToShortest(d, &builder);
? conv.ToShortestSingle(d, &builder)
: conv.ToShortest(d, &builder);
if (!valid) {
std::cerr << "problem with " << d << std::endl;
std::abort();
Expand Down Expand Up @@ -284,24 +264,101 @@ int yy_double(T d, std::span<char>& buffer) {
#endif
}

template<arithmetic_float T>
int std_to_chars(T d, std::span<char>& buffer) {
#if TO_CHARS_SUPPORTED
const auto [p, ec]
= std::to_chars(buffer.data(), buffer.data() + buffer.size(), d);
if (ec != std::errc()) {
std::cerr << "problem with " << d << std::endl;
std::abort();
}
return p - buffer.data();
#else
std::cerr << "std::to_chars not supported" << std::endl;
std::abort();
#endif
}

} // namespace BenchmarksShortest

namespace BenchmarkFixedSize {

template<arithmetic_float T>
int dragon4(T d, std::span<char>& buffer) {
if constexpr (std::is_same_v<T, float>)
return PrintFloat32(buffer.data(), buffer.size(), d,
PrintFloatFormat_Positional, BenchArgs<T>::fixedSize);
else
return PrintFloat64(buffer.data(), buffer.size(), d,
PrintFloatFormat_Positional, BenchArgs<T>::fixedSize);
}

template<arithmetic_float T>
int netlib(T d, std::span<char>& buffer) {
#if NETLIB_SUPPORTED
char* res;
if constexpr (std::is_same_v<T, float>)
res = g_ffmt(buffer.data(), &d, BenchArgs<T>::fixedSize, buffer.size());
else
res = g_dfmt(buffer.data(), &d, BenchArgs<T>::fixedSize, buffer.size());
*res = '\0';
return res - buffer.data() + 1;
#else
std::cerr << "netlib not supported" << std::endl;
std::abort();
#endif
}

template<arithmetic_float T>
int abseil(T d, std::span<char>& buffer) {
// StrAppend is faster but only outputs 6 digits after the decimal point
// std::string s;
// absl::StrAppend(&s, d);
// std::copy(s.begin(), s.end(), buffer.begin());
// return size(s);
if constexpr (std::is_same_v<T, float>)
return absl::SNPrintF(buffer.data(), buffer.size(), "%.9g", d);
else
return absl::SNPrintF(buffer.data(), buffer.size(), "%.17g", d);
return absl::SNPrintF(buffer.data(), buffer.size(),
BenchArgs<T>::formatStr, d);
}

template<arithmetic_float T>
int snprintf(T d, std::span<char>& buffer) {
return std::snprintf(buffer.data(), buffer.size(),
BenchArgs<T>::formatStr, d);
}

template<arithmetic_float T>
int fmt_format(T d, std::span<char>& buffer) {
const auto it = fmt::format_to(buffer.begin(),
fmt::runtime(BenchArgs<T>::formatStrStr), d);
return std::distance(buffer.begin(), it);
}

template<arithmetic_float T>
int ryu(T d, std::span<char>& buffer) {
return d2fixed_buffered_n(d, BenchArgs<T>::fixedSize, buffer.data());
}

template<arithmetic_float T>
int double_conversion(T d, std::span<char>& buffer) {
const static double_conversion::DoubleToStringConverter conv(
double_conversion::DoubleToStringConverter::NO_FLAGS, "inf", "nan", 'e',
-6, 21, BenchArgs<T>::fixedSize, BenchArgs<T>::fixedSize);

double_conversion::StringBuilder builder(buffer.data(), buffer.size());
if (!conv.ToPrecision(d, BenchArgs<T>::fixedSize, &builder)) {
std::cerr << "problem with " << d << std::endl;
std::abort();
}
return strlen(builder.Finalize());
}

template<arithmetic_float T>
int std_to_chars(T d, std::span<char>& buffer) {
#if TO_CHARS_SUPPORTED
const auto [p, ec]
= std::to_chars(buffer.data(), buffer.data() + buffer.size(), d);
= std::to_chars(buffer.data(), buffer.data() + buffer.size(), d,
std::chars_format::general, BenchArgs<T>::fixedSize);
if (ec != std::errc()) {
std::cerr << "problem with " << d << std::endl;
std::abort();
Expand All @@ -313,30 +370,59 @@ int std_to_chars(T d, std::span<char>& buffer) {
#endif
}

} // namespace BenchmarksShortest

template <typename T>
auto wrap(int (*fn)(T, std::span<char>&)) {
return [fn](T v, std::span<char>& buf) -> int {
return fn(v, buf);
};
}

template <arithmetic_float T>
std::array<BenchArgs<T>, Benchmarks::COUNT> initArgs(bool errol = false) {
std::array<BenchArgs<T>, Benchmarks::COUNT> args;
args[Benchmarks::DRAGON4] = { "dragon4" , Benchmarks::dragon4<T> , true , 10 };
args[Benchmarks::ERROL3] = { "errol3" , Benchmarks::errol3<T> , errol };
args[Benchmarks::TO_STRING] = { "std::to_string" , Benchmarks::to_string<T> , ERROL_SUPPORTED };
args[Benchmarks::FMT_FORMAT] = { "fmt::format" , Benchmarks::fmt_format<T> , true };
args[Benchmarks::NETLIB] = { "netlib" , Benchmarks::netlib<T> , NETLIB_SUPPORTED && std::is_same_v<T, double>, 10 };
args[Benchmarks::SNPRINTF] = { "snprintf" , Benchmarks::snprintf<T> , true };
args[Benchmarks::GRISU2] = { "grisu2" , Benchmarks::grisu2<T> , std::is_same_v<T, double> };
args[Benchmarks::GRISU_EXACT] = { "grisu_exact" , Benchmarks::grisu_exact<T> , true };
args[Benchmarks::SCHUBFACH] = { "schubfach" , Benchmarks::schubfach<T> , true };
args[Benchmarks::DRAGONBOX] = { "dragonbox" , Benchmarks::dragonbox<T> , true };
args[Benchmarks::RYU] = { "ryu" , Benchmarks::ryu<T> , true };
args[Benchmarks::TEJU_JAGUA] = { "teju_jagua" , Benchmarks::teju_jagua<T> , true };
args[Benchmarks::DOUBLE_CONVERSION] = { "double_conversion" , Benchmarks::double_conversion<T> , true };
args[Benchmarks::ABSEIL] = { "abseil" , Benchmarks::abseil<T> , ABSEIL_SUPPORTED };
args[Benchmarks::STD_TO_CHARS] = { "std::to_chars" , Benchmarks::std_to_chars<T> , TO_CHARS_SUPPORTED };
args[Benchmarks::GRISU3] = { "grisu3" , Benchmarks::grisu3<T> , std::is_same_v<T, double> };
args[Benchmarks::SWIFT_DTOA] = { "SwiftDtoa" , Benchmarks::swiftDtoa<T> , SWIFT_LIB_SUPPORTED };
args[Benchmarks::YY_DOUBLE] = { "yy_double" , Benchmarks::yy_double<T> , YY_DOUBLE_SUPPORTED && std::is_same_v<T, double> };
std::vector<BenchArgs<T>> initArgs(bool use_errol = false, size_t repeat = 0, size_t fixed_size = 0) {
std::vector<BenchArgs<T>> args;
if (fixed_size == 0) { // shortest-length representation
namespace s = BenchmarkShortest;
args.emplace_back("dragon4" , wrap(s::dragon4<T>) , true , 10);
args.emplace_back("netlib" , wrap(s::netlib<T>) , NETLIB_SUPPORTED && std::is_same_v<T, double> , 10);
args.emplace_back("errol3" , wrap(s::errol3<T>) , ERROL_SUPPORTED && use_errol);
args.emplace_back("fmt_format" , wrap(s::fmt_format<T>) , true);
// args.emplace_back("grisu2" , wrap(s::grisu2<T>) , std::is_same_v<T, double>);
args.emplace_back("grisu3" , wrap(s::grisu3<T>) , std::is_same_v<T, double>);
args.emplace_back("grisu_exact" , wrap(s::grisu_exact<T>) , true);
args.emplace_back("schubfach" , wrap(s::schubfach<T>) , true);
args.emplace_back("dragonbox" , wrap(s::dragonbox<T>) , true);
args.emplace_back("ryu" , wrap(s::ryu<T>) , true);
args.emplace_back("teju_jagua" , wrap(s::teju_jagua<T>) , true);
args.emplace_back("double_conversion" , wrap(s::double_conversion<T>) , true);
args.emplace_back("swiftDtoa" , wrap(s::swiftDtoa<T>) , SWIFT_LIB_SUPPORTED);
args.emplace_back("yy_double" , wrap(s::yy_double<T>) , YY_DOUBLE_SUPPORTED && std::is_same_v<T, double>);
args.emplace_back("std::to_chars" , wrap(s::std_to_chars<T>) , TO_CHARS_SUPPORTED);
// to_string, snprintf and abseil do not support shortest-length representation
// grisu2 does not round-trip correctly
} else { // fixed-length representation
fmt::println("# testing fixed-size output to {} digits", fixed_size);
BenchArgs<T>::initFixedSize(fixed_size);

namespace f = BenchmarkFixedSize;
args.emplace_back("dragon4" , wrap(f::dragon4<T>) , true , 10);
args.emplace_back("netlib" , wrap(f::netlib<T>) , NETLIB_SUPPORTED , 10);
args.emplace_back("abseil" , wrap(f::abseil<T>) , ABSEIL_SUPPORTED);
args.emplace_back("snprintf" , wrap(f::snprintf<T>) , true);
args.emplace_back("fmt_format" , wrap(f::fmt_format<T>) , true);
args.emplace_back("ryu" , wrap(f::ryu<T>) , std::is_same_v<T, double>);
args.emplace_back("double_conversion" , wrap(f::double_conversion<T>) , true);
args.emplace_back("std::to_chars" , wrap(f::std_to_chars<T>) , TO_CHARS_SUPPORTED);
}

if (repeat > 0) {
fmt::println("# forcing repeat count to {}", repeat);
for (auto &arg : args)
arg.testRepeat = repeat;
}

return args;
};

} // namespace Benchmarks

#endif
Loading