From 31e26648216f380b2056c7ff3e36d605eec15ea8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ja=C3=ABl=20Champagne=20Gareau?= Date: Tue, 6 May 2025 00:08:13 -0400 Subject: [PATCH 1/3] add a flag to toggle shortest-length testing --- README.md | 10 ++++++++ benchmarks/algorithms.h | 46 ++++++++++++++++++---------------- benchmarks/benchmark.cpp | 8 ++++-- benchmarks/random_generators.h | 4 +-- 4 files changed, 43 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index f230ff1..edd3560 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,16 @@ cmake --build build --config Release ./build/benchmarks/thoroughfloat64 ``` +## Fixed-point evaluation + +By default, we compare algorithms that output shortest string representation +which can round-trip to the original floating-point value. We can also compare +algorithms that output fixed-length representation of a given length: + +``` +./build/benchmarks/benchmark -f data/canada.txt -F [length] +``` + ## Other existing benchmarks - [dtoa Benchmark](https://github.com/miloyip/dtoa-benchmark) diff --git a/benchmarks/algorithms.h b/benchmarks/algorithms.h index 4d8fd8e..233e025 100644 --- a/benchmarks/algorithms.h +++ b/benchmarks/algorithms.h @@ -314,27 +314,31 @@ int std_to_chars(T d, std::span& buffer) { } template -std::array, Benchmarks::COUNT> initArgs(bool errol = false) { - std::array, Benchmarks::COUNT> args; - args[Benchmarks::DRAGON4] = { "dragon4" , Benchmarks::dragon4 , true , 10 }; - args[Benchmarks::ERROL3] = { "errol3" , Benchmarks::errol3 , errol }; - args[Benchmarks::TO_STRING] = { "std::to_string" , Benchmarks::to_string , ERROL_SUPPORTED }; - args[Benchmarks::FMT_FORMAT] = { "fmt::format" , Benchmarks::fmt_format , true }; - args[Benchmarks::NETLIB] = { "netlib" , Benchmarks::netlib , NETLIB_SUPPORTED && std::is_same_v, 10 }; - args[Benchmarks::SNPRINTF] = { "snprintf" , Benchmarks::snprintf , true }; - args[Benchmarks::GRISU2] = { "grisu2" , Benchmarks::grisu2 , std::is_same_v }; - args[Benchmarks::GRISU_EXACT] = { "grisu_exact" , Benchmarks::grisu_exact , true }; - args[Benchmarks::SCHUBFACH] = { "schubfach" , Benchmarks::schubfach , true }; - args[Benchmarks::DRAGONBOX] = { "dragonbox" , Benchmarks::dragonbox , true }; - args[Benchmarks::RYU] = { "ryu" , Benchmarks::ryu , true }; - args[Benchmarks::TEJU_JAGUA] = { "teju_jagua" , Benchmarks::teju_jagua , true }; - args[Benchmarks::DOUBLE_CONVERSION] = { "double_conversion" , Benchmarks::double_conversion , true }; - args[Benchmarks::ABSEIL] = { "abseil" , Benchmarks::abseil , ABSEIL_SUPPORTED }; - args[Benchmarks::STD_TO_CHARS] = { "std::to_chars" , Benchmarks::std_to_chars , TO_CHARS_SUPPORTED }; - args[Benchmarks::GRISU3] = { "grisu3" , Benchmarks::grisu3 , std::is_same_v }; - args[Benchmarks::SWIFT_DTOA] = { "SwiftDtoa" , Benchmarks::swiftDtoa , SWIFT_LIB_SUPPORTED }; - args[Benchmarks::YY_DOUBLE] = { "yy_double" , Benchmarks::yy_double , YY_DOUBLE_SUPPORTED && std::is_same_v }; - return args; +std::array, Benchmarks::COUNT> initArgs(size_t fixed_size, bool use_errol = false) { + if (fixed_size == 0) { // shortest length representation + std::array, Benchmarks::COUNT> args; + args[Benchmarks::DRAGON4] = { "dragon4" , Benchmarks::dragon4 , true , 10 }; + args[Benchmarks::ERROL3] = { "errol3" , Benchmarks::errol3 , ERROL_SUPPORTED && use_errol }; + args[Benchmarks::TO_STRING] = { "std::to_string" , Benchmarks::to_string , true }; + args[Benchmarks::FMT_FORMAT] = { "fmt::format" , Benchmarks::fmt_format , true }; + args[Benchmarks::NETLIB] = { "netlib" , Benchmarks::netlib , NETLIB_SUPPORTED && std::is_same_v, 10 }; + args[Benchmarks::SNPRINTF] = { "snprintf" , Benchmarks::snprintf , true }; + args[Benchmarks::GRISU2] = { "grisu2" , Benchmarks::grisu2 , std::is_same_v }; + args[Benchmarks::GRISU_EXACT] = { "grisu_exact" , Benchmarks::grisu_exact , true }; + args[Benchmarks::SCHUBFACH] = { "schubfach" , Benchmarks::schubfach , true }; + args[Benchmarks::DRAGONBOX] = { "dragonbox" , Benchmarks::dragonbox , true }; + args[Benchmarks::RYU] = { "ryu" , Benchmarks::ryu , true }; + args[Benchmarks::TEJU_JAGUA] = { "teju_jagua" , Benchmarks::teju_jagua , true }; + args[Benchmarks::DOUBLE_CONVERSION] = { "double_conversion" , Benchmarks::double_conversion , true }; + args[Benchmarks::ABSEIL] = { "abseil" , Benchmarks::abseil , ABSEIL_SUPPORTED }; + args[Benchmarks::STD_TO_CHARS] = { "std::to_chars" , Benchmarks::std_to_chars , TO_CHARS_SUPPORTED }; + args[Benchmarks::GRISU3] = { "grisu3" , Benchmarks::grisu3 , std::is_same_v }; + args[Benchmarks::SWIFT_DTOA] = { "SwiftDtoa" , Benchmarks::swiftDtoa , SWIFT_LIB_SUPPORTED }; + args[Benchmarks::YY_DOUBLE] = { "yy_double" , Benchmarks::yy_double , YY_DOUBLE_SUPPORTED && std::is_same_v }; + return args; + } else { // fixed-length representation + throw std::runtime_error("fixed length representation not yet implemented"); + } }; } // namespace Benchmarks diff --git a/benchmarks/benchmark.cpp b/benchmarks/benchmark.cpp index b8e2937..31b55a3 100644 --- a/benchmarks/benchmark.cpp +++ b/benchmarks/benchmark.cpp @@ -147,6 +147,8 @@ int main(int argc, char **argv) { options.add_options() ("f,file", "File name.", cxxopts::value()->default_value("")) + ("F,fixed", "Fixed-point representation.", + cxxopts::value()->default_value("0")) ("v,volume", "Volume (number of floats generated).", cxxopts::value()->default_value("100000")) ("m,model", "Random Model.", @@ -198,10 +200,11 @@ int main(int argc, char **argv) { std::variant, Benchmarks::COUNT>, std::array, Benchmarks::COUNT>> algorithms; const bool errol = result["errol"].as(); + const size_t fixed_size = result["fixed"].as(); if (single) - algorithms = Benchmarks::initArgs(errol); + algorithms = Benchmarks::initArgs(fixed_size, errol); else - algorithms = Benchmarks::initArgs(errol); + algorithms = Benchmarks::initArgs(fixed_size, errol); if(repeat > 0) { fmt::println("# forcing repeat count to {}", repeat); @@ -233,6 +236,7 @@ int main(int argc, char **argv) { fmt::println("\nEXAMPLES:"); fmt::println(" ./benchmark --single # Run benchmark with single precision (float)"); fmt::println(" ./benchmark --file=data/canada.txt # Run benchmark using numbers from a file"); + fmt::println(" ./benchmark --fixed=10 # Test fixed-point representation instead of shortest length"); fmt::println(" ./benchmark --test # Test correctness instead of performance"); fmt::println(" ./benchmark --volume=1000 --model=uniform # Generate 1000 uniform random numbers"); fmt::println(" ./benchmark --algo-filter=ryu,grisu # Only test algorithms containing 'ryu' or 'grisu'"); diff --git a/benchmarks/random_generators.h b/benchmarks/random_generators.h index 17e21ac..b1406b9 100644 --- a/benchmarks/random_generators.h +++ b/benchmarks/random_generators.h @@ -33,8 +33,8 @@ template struct integer_uniform_generator : float_number_generator { std::random_device rd; std::mt19937_64 gen; - std::uniform_int_distribution dis; - explicit integer_uniform_generator(uint64_t a = 0, uint64_t b = 1) + std::uniform_int_distribution dis; + explicit integer_uniform_generator(long a = LONG_MIN, long b = LONG_MAX) : rd(), gen(rd()), dis(a, b) {} std::string describe() override { return std::string( From 947dfddb6e7d76ba8ae139c66b4fa61bdd34e87a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ja=C3=ABl=20Champagne=20Gareau?= Date: Thu, 8 May 2025 22:10:42 -0400 Subject: [PATCH 2/3] Refactor to make it easier to test fixed-length --- README.md | 3 + benchmarks/algorithms.h | 164 +++++++++++++++++-------------- benchmarks/benchmark.cpp | 25 ++--- benchmarks/benchutil.h | 14 ++- benchmarks/exhaustivefloat32.cpp | 2 - 5 files changed, 106 insertions(+), 102 deletions(-) diff --git a/README.md b/README.md index edd3560..da4b27e 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,9 @@ algorithms that output fixed-length representation of a given length: ./build/benchmarks/benchmark -f data/canada.txt -F [length] ``` +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) diff --git a/benchmarks/algorithms.h b/benchmarks/algorithms.h index 233e025..798589d 100644 --- a/benchmarks/algorithms.h +++ b/benchmarks/algorithms.h @@ -44,44 +44,24 @@ #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 struct BenchArgs { using Type = T; + using BenchFn = std::function&, size_t fixed_size)>; - BenchArgs(const std::string& name = {}, int (*func)(T, std::span&) = {}, - bool used = true, size_t testRepeat = 100) - : name(name), func(func), used(used), testRepeat(testRepeat) {} + BenchArgs(const std::string& name = {}, BenchFn func = {}, bool used = true, + size_t testRepeat = 100, size_t fixedSize = 9) + : name(name), func(func), used(used), testRepeat(testRepeat), fixedSize(fixedSize) {} std::string name{}; - int (*func)(T, std::span&){}; + BenchFn func{}; bool used{}; size_t testRepeat{100}; + size_t fixedSize{9}; }; +namespace BenchmarkShortest { + template int dragon4(T d, std::span& buffer) { if constexpr (std::is_same_v) @@ -187,14 +167,6 @@ int netlib(T d, std::span& buffer) { #endif } -template -int snprintf(T d, std::span& buffer) { - if constexpr (std::is_same_v) - return std::snprintf(buffer.data(), buffer.size(), "%.9g", d); - else - return std::snprintf(buffer.data(), buffer.size(), "%.17g", d); -} - // grisu2 is hardcoded for double. template int grisu2(T d, std::span& buffer) { @@ -284,19 +256,6 @@ int yy_double(T d, std::span& buffer) { #endif } -template -int abseil(T d, std::span& 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) - return absl::SNPrintF(buffer.data(), buffer.size(), "%.9g", d); - else - return absl::SNPrintF(buffer.data(), buffer.size(), "%.17g", d); -} - template int std_to_chars(T d, std::span& buffer) { #if TO_CHARS_SUPPORTED @@ -313,34 +272,91 @@ int std_to_chars(T d, std::span& buffer) { #endif } +} // namespace BenchmarksShortest + +namespace BenchmarkFixedSize { + +template +int abseil(T d, std::span& buffer, size_t fixed_size) { + // 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) + return absl::SNPrintF(buffer.data(), buffer.size(), "%.9g", d); + else + return absl::SNPrintF(buffer.data(), buffer.size(), "%.17g", d); +} + +template +int snprintf(T d, std::span& buffer, size_t fixed_size) { + if constexpr (std::is_same_v) + return std::snprintf(buffer.data(), buffer.size(), "%.9g", d); + else + return std::snprintf(buffer.data(), buffer.size(), "%.17g", d); +} + +} // namespace BenchmarksShortest + +template +auto make_shortest_adapter(int (*fn)(T, std::span&)) { + return [fn](T v, std::span& buf, size_t /*fixed_size*/) -> int { + return fn(v, buf); + }; +} + +template +auto make_fixed_adapter(int (*fn)(T, std::span&, size_t)) { + return [fn](T v, std::span& buf, size_t fixed_size) -> int { + return fn(v, buf, fixed_size); + }; +} + template -std::array, Benchmarks::COUNT> initArgs(size_t fixed_size, bool use_errol = false) { - if (fixed_size == 0) { // shortest length representation - std::array, Benchmarks::COUNT> args; - args[Benchmarks::DRAGON4] = { "dragon4" , Benchmarks::dragon4 , true , 10 }; - args[Benchmarks::ERROL3] = { "errol3" , Benchmarks::errol3 , ERROL_SUPPORTED && use_errol }; - args[Benchmarks::TO_STRING] = { "std::to_string" , Benchmarks::to_string , true }; - args[Benchmarks::FMT_FORMAT] = { "fmt::format" , Benchmarks::fmt_format , true }; - args[Benchmarks::NETLIB] = { "netlib" , Benchmarks::netlib , NETLIB_SUPPORTED && std::is_same_v, 10 }; - args[Benchmarks::SNPRINTF] = { "snprintf" , Benchmarks::snprintf , true }; - args[Benchmarks::GRISU2] = { "grisu2" , Benchmarks::grisu2 , std::is_same_v }; - args[Benchmarks::GRISU_EXACT] = { "grisu_exact" , Benchmarks::grisu_exact , true }; - args[Benchmarks::SCHUBFACH] = { "schubfach" , Benchmarks::schubfach , true }; - args[Benchmarks::DRAGONBOX] = { "dragonbox" , Benchmarks::dragonbox , true }; - args[Benchmarks::RYU] = { "ryu" , Benchmarks::ryu , true }; - args[Benchmarks::TEJU_JAGUA] = { "teju_jagua" , Benchmarks::teju_jagua , true }; - args[Benchmarks::DOUBLE_CONVERSION] = { "double_conversion" , Benchmarks::double_conversion , true }; - args[Benchmarks::ABSEIL] = { "abseil" , Benchmarks::abseil , ABSEIL_SUPPORTED }; - args[Benchmarks::STD_TO_CHARS] = { "std::to_chars" , Benchmarks::std_to_chars , TO_CHARS_SUPPORTED }; - args[Benchmarks::GRISU3] = { "grisu3" , Benchmarks::grisu3 , std::is_same_v }; - args[Benchmarks::SWIFT_DTOA] = { "SwiftDtoa" , Benchmarks::swiftDtoa , SWIFT_LIB_SUPPORTED }; - args[Benchmarks::YY_DOUBLE] = { "yy_double" , Benchmarks::yy_double , YY_DOUBLE_SUPPORTED && std::is_same_v }; - return args; +std::vector> initArgs(bool use_errol = false, size_t repeat = 0, size_t fixed_size = 0) { + std::vector> args; + if (fixed_size == 0) { // shortest-length representation + auto&& wrap = make_shortest_adapter; + namespace s = BenchmarkShortest; + args.emplace_back("dragon4" , wrap(s::dragon4) , true , 10); + args.emplace_back("netlib" , wrap(s::netlib) , NETLIB_SUPPORTED && std::is_same_v , 10); + args.emplace_back("errol3" , wrap(s::errol3) , ERROL_SUPPORTED && use_errol); + args.emplace_back("fmt_format" , wrap(s::fmt_format) , true); + args.emplace_back("grisu2" , wrap(s::grisu2) , std::is_same_v); + args.emplace_back("grisu3" , wrap(s::grisu3) , std::is_same_v); + args.emplace_back("grisu_exact" , wrap(s::grisu_exact) , true); + args.emplace_back("schubfach" , wrap(s::schubfach) , true); + args.emplace_back("dragonbox" , wrap(s::dragonbox) , true); + args.emplace_back("ryu" , wrap(s::ryu) , true); + args.emplace_back("teju_jagua" , wrap(s::teju_jagua) , true); + args.emplace_back("double_conversion" , wrap(s::double_conversion) , true); + args.emplace_back("swiftDtoa" , wrap(s::swiftDtoa) , SWIFT_LIB_SUPPORTED); + args.emplace_back("yy_double" , wrap(s::yy_double) , YY_DOUBLE_SUPPORTED && std::is_same_v); + args.emplace_back("std::to_chars" , wrap(s::std_to_chars) , TO_CHARS_SUPPORTED); + + // to_string, snprintf and abseil do not support shortest-length representation } else { // fixed-length representation - throw std::runtime_error("fixed length representation not yet implemented"); + auto&& wrap = make_fixed_adapter; + namespace f = BenchmarkFixedSize; + args.emplace_back("snprintf" , wrap(f::snprintf) , true); + args.emplace_back("abseil" , wrap(f::abseil) , ABSEIL_SUPPORTED); + + // to_string is hard-coded for 6 digits after the decimal point + // args.emplace_back("to_string", BenchmarkFixedSize::to_string, true); + + fmt::println("# testing fixed-size output to {} digits", fixed_size); + for (auto &arg : args) + arg.fixedSize = fixed_size; } -}; -} // namespace Benchmarks + if (repeat > 0) { + fmt::println("# forcing repeat count to {}", repeat); + for (auto &arg : args) + arg.testRepeat = repeat; + } + + return args; +}; #endif diff --git a/benchmarks/benchmark.cpp b/benchmarks/benchmark.cpp index 31b55a3..4f386b6 100644 --- a/benchmarks/benchmark.cpp +++ b/benchmarks/benchmark.cpp @@ -25,11 +25,9 @@ #include #include -using Benchmarks::BenchArgs; - template void evaluateProperties(const std::vector> &lines, - const std::array, Benchmarks::COUNT> &args, + const std::vector> &args, const std::vector &algo_filter) { evaluate_properties_helper(lines, algo_filter, args); } @@ -44,7 +42,7 @@ struct diy_float_t { template void process(const std::vector> &lines, - const std::array, Benchmarks::COUNT> &args, + const std::vector> &args, const std::vector &algo_filter) { // We have a special algorithm for the string generation: if (!algo_filtered_out("just_string", algo_filter)) { @@ -95,7 +93,7 @@ void process(const std::vector> &lines, char buf[100]; std::span bufspan(buf, sizeof(buf)); for (const auto d : lines) - volume += algo.func(d.value, bufspan); + volume += algo.func(d.value, bufspan, algo.fixedSize); return volume; }, algo.testRepeat); } @@ -170,7 +168,6 @@ int main(int argc, char **argv) { fmt::print("{}\n", options.help()); return EXIT_SUCCESS; } - const size_t repeat = result["repeat"].as(); const bool single = result["single"].as(); const auto filter = result.count("algo-filter") ? result["algo-filter"].as>() @@ -197,22 +194,14 @@ int main(int argc, char **argv) { numbers = fileload(filename); } - std::variant, Benchmarks::COUNT>, - std::array, Benchmarks::COUNT>> algorithms; + std::variant>, std::vector>> algorithms; const bool errol = result["errol"].as(); + const size_t repeat = result["repeat"].as(); const size_t fixed_size = result["fixed"].as(); if (single) - algorithms = Benchmarks::initArgs(fixed_size, errol); + algorithms = initArgs(errol, repeat, fixed_size); else - algorithms = Benchmarks::initArgs(fixed_size, errol); - - if(repeat > 0) { - fmt::println("# forcing repeat count to {}", repeat); - std::visit([repeat](auto &args) { - for (auto &arg : args) - arg.testRepeat = repeat; - }, algorithms); - } + algorithms = initArgs(errol, repeat, fixed_size); const bool test = result["test"].as(); std::visit([test, &filter](const auto &lines, const auto &args) { diff --git a/benchmarks/benchutil.h b/benchmarks/benchutil.h index c76e6aa..186b013 100644 --- a/benchmarks/benchutil.h +++ b/benchmarks/benchutil.h @@ -13,8 +13,6 @@ #include "algorithms.h" #include "counters/event_counter.h" -using Benchmarks::BenchArgs; - event_collector collector; bool algo_filtered_out(const std::string &algo_name, @@ -50,11 +48,11 @@ concept TestCaseRange template requires TestCaseRange void evaluate_properties_helper(Range&& cases, const std::vector &algo_filter, - std::variant, Benchmarks::COUNT>, bool> argsOpt) { + std::variant>, bool> argsOpt) { fmt::println("{:20} {:20}", "Algorithm", "Valid shortest serialization"); const auto args = std::holds_alternative(argsOpt) - ? Benchmarks::initArgs(std::get(argsOpt)) - : std::get, Benchmarks::COUNT>>(argsOpt); + ? initArgs(std::get(argsOpt)) + : std::get>>(argsOpt); // Get number of cases for progress display uint64_t total = 0; @@ -69,7 +67,7 @@ void evaluate_properties_helper(Range&& cases, fmt::println("# skipping {}", algo.name); continue; } - if (algo.func == Benchmarks::dragonbox) { + if (algo.name == "dragonbox") { fmt::println("# skipping {} because it is the reference.", algo.name); continue; } @@ -102,8 +100,8 @@ void evaluate_properties_helper(Range&& cases, // the shortest representation, which is not necessarily the same as the // representation using the fewest significant digits. // So we use dragonbox, which serves as the reference implementation. - const size_t vRef = Benchmarks::dragonbox(d, bufRef); - const size_t vAlgo = algo.func(d, bufAlgo); + const size_t vRef = BenchmarkShortest::dragonbox(d, bufRef); + const size_t vAlgo = algo.func(d, bufAlgo, algo.fixedSize); std::string_view svRef{bufRef.data(), vRef}, svAlgo{bufAlgo.data(), vAlgo}; diff --git a/benchmarks/exhaustivefloat32.cpp b/benchmarks/exhaustivefloat32.cpp index d284c1d..85866f9 100644 --- a/benchmarks/exhaustivefloat32.cpp +++ b/benchmarks/exhaustivefloat32.cpp @@ -8,8 +8,6 @@ #include "floatutils.h" #include "benchutil.h" -using Benchmarks::BenchArgs; - void run_exhaustive32(bool errol, const std::vector& algo_filter = {}) { static_assert(sizeof(float) == sizeof(uint32_t)); auto floats_view From 0d7690ec8e3a8cca674dc6683d3eefd5f4e88fd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ja=C3=ABl=20Champagne=20Gareau?= Date: Sat, 10 May 2025 14:43:15 -0400 Subject: [PATCH 3/3] add fixed-precision wrappers of compat algorithms --- README.md | 8 +- benchmarks/algorithms.h | 210 +++++++++++++++++++++++++-------------- benchmarks/benchmark.cpp | 2 +- benchmarks/benchutil.h | 2 +- 4 files changed, 144 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index da4b27e..3efd90c 100644 --- a/README.md +++ b/README.md @@ -92,14 +92,14 @@ cmake --build build --config Release ./build/benchmarks/thoroughfloat64 ``` -## Fixed-point evaluation +## Fixed-precision evaluation -By default, we compare algorithms that output shortest string representation +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-length representation of a given length: +algorithms that output fixed-precision representation of a given precision: ``` -./build/benchmarks/benchmark -f data/canada.txt -F [length] +./build/benchmarks/benchmark -f data/canada.txt -F [precision] ``` Note that this only works when we are comparing speeds, not measuring properties diff --git a/benchmarks/algorithms.h b/benchmarks/algorithms.h index 798589d..074fc49 100644 --- a/benchmarks/algorithms.h +++ b/benchmarks/algorithms.h @@ -47,17 +47,24 @@ template struct BenchArgs { using Type = T; - using BenchFn = std::function&, size_t fixed_size)>; + using BenchFn = std::function&)>; - BenchArgs(const std::string& name = {}, BenchFn func = {}, bool used = true, - size_t testRepeat = 100, size_t fixedSize = 9) - : name(name), func(func), used(used), testRepeat(testRepeat), fixedSize(fixedSize) {} + 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{}; BenchFn func{}; bool used{}; size_t testRepeat{100}; - size_t fixedSize{9}; + + 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 { @@ -72,32 +79,6 @@ int dragon4(T d, std::span& buffer) { PrintFloatFormat_Positional, -1); } -// No errol3 implementation optimized for float instead of double ? -template -int errol3(T d, std::span& 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 -int to_string(T d, std::span& buffer) { - const std::string s = std::to_string(d); - std::copy(s.begin(), s.end(), buffer.begin()); - return s.size(); -} - -template -int fmt_format(T d, std::span& 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 int netlib(T d, std::span& buffer) { @@ -137,7 +118,7 @@ int netlib(T d, std::span& 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) { @@ -167,6 +148,31 @@ int netlib(T d, std::span& buffer) { #endif } +// No errol3 implementation optimized for float instead of double ? +template +int errol3(T d, std::span& 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 +int to_string(T d, std::span& buffer) { + const std::string s = std::to_string(d); + std::copy(s.begin(), s.end(), buffer.begin()); + return s.size(); +} + +template +int fmt_format(T d, std::span& buffer) { + const auto it = fmt::format_to(buffer.data(), "{}", d); + return std::distance(buffer.data(), it); +} + // grisu2 is hardcoded for double. template int grisu2(T d, std::span& buffer) { @@ -218,13 +224,15 @@ int teju_jagua(T d, std::span& buffer) { template int double_conversion(T d, std::span& 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 - ? 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(); @@ -277,39 +285,97 @@ int std_to_chars(T d, std::span& buffer) { namespace BenchmarkFixedSize { template -int abseil(T d, std::span& buffer, size_t fixed_size) { +int dragon4(T d, std::span& buffer) { + if constexpr (std::is_same_v) + return PrintFloat32(buffer.data(), buffer.size(), d, + PrintFloatFormat_Positional, BenchArgs::fixedSize); + else + return PrintFloat64(buffer.data(), buffer.size(), d, + PrintFloatFormat_Positional, BenchArgs::fixedSize); +} + +template +int netlib(T d, std::span& buffer) { +#if NETLIB_SUPPORTED + char* res; + if constexpr (std::is_same_v) + res = g_ffmt(buffer.data(), &d, BenchArgs::fixedSize, buffer.size()); + else + res = g_dfmt(buffer.data(), &d, BenchArgs::fixedSize, buffer.size()); + *res = '\0'; + return res - buffer.data() + 1; +#else + std::cerr << "netlib not supported" << std::endl; + std::abort(); +#endif +} + +template +int abseil(T d, std::span& 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) - 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::formatStr, d); } template -int snprintf(T d, std::span& buffer, size_t fixed_size) { - if constexpr (std::is_same_v) - return std::snprintf(buffer.data(), buffer.size(), "%.9g", d); - else - return std::snprintf(buffer.data(), buffer.size(), "%.17g", d); +int snprintf(T d, std::span& buffer) { + return std::snprintf(buffer.data(), buffer.size(), + BenchArgs::formatStr, d); } -} // namespace BenchmarksShortest +template +int fmt_format(T d, std::span& buffer) { + const auto it = fmt::format_to(buffer.begin(), + fmt::runtime(BenchArgs::formatStrStr), d); + return std::distance(buffer.begin(), it); +} -template -auto make_shortest_adapter(int (*fn)(T, std::span&)) { - return [fn](T v, std::span& buf, size_t /*fixed_size*/) -> int { - return fn(v, buf); - }; +template +int ryu(T d, std::span& buffer) { + return d2fixed_buffered_n(d, BenchArgs::fixedSize, buffer.data()); } +template +int double_conversion(T d, std::span& buffer) { + const static double_conversion::DoubleToStringConverter conv( + double_conversion::DoubleToStringConverter::NO_FLAGS, "inf", "nan", 'e', + -6, 21, BenchArgs::fixedSize, BenchArgs::fixedSize); + + double_conversion::StringBuilder builder(buffer.data(), buffer.size()); + if (!conv.ToPrecision(d, BenchArgs::fixedSize, &builder)) { + std::cerr << "problem with " << d << std::endl; + std::abort(); + } + return strlen(builder.Finalize()); +} + +template +int std_to_chars(T d, std::span& buffer) { +#if TO_CHARS_SUPPORTED + const auto [p, ec] + = std::to_chars(buffer.data(), buffer.data() + buffer.size(), d, + std::chars_format::general, BenchArgs::fixedSize); + 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 + template -auto make_fixed_adapter(int (*fn)(T, std::span&, size_t)) { - return [fn](T v, std::span& buf, size_t fixed_size) -> int { - return fn(v, buf, fixed_size); +auto wrap(int (*fn)(T, std::span&)) { + return [fn](T v, std::span& buf) -> int { + return fn(v, buf); }; } @@ -317,13 +383,12 @@ template std::vector> initArgs(bool use_errol = false, size_t repeat = 0, size_t fixed_size = 0) { std::vector> args; if (fixed_size == 0) { // shortest-length representation - auto&& wrap = make_shortest_adapter; namespace s = BenchmarkShortest; args.emplace_back("dragon4" , wrap(s::dragon4) , true , 10); args.emplace_back("netlib" , wrap(s::netlib) , NETLIB_SUPPORTED && std::is_same_v , 10); args.emplace_back("errol3" , wrap(s::errol3) , ERROL_SUPPORTED && use_errol); args.emplace_back("fmt_format" , wrap(s::fmt_format) , true); - args.emplace_back("grisu2" , wrap(s::grisu2) , std::is_same_v); + // args.emplace_back("grisu2" , wrap(s::grisu2) , std::is_same_v); args.emplace_back("grisu3" , wrap(s::grisu3) , std::is_same_v); args.emplace_back("grisu_exact" , wrap(s::grisu_exact) , true); args.emplace_back("schubfach" , wrap(s::schubfach) , true); @@ -334,26 +399,27 @@ std::vector> initArgs(bool use_errol = false, size_t repeat = 0, si args.emplace_back("swiftDtoa" , wrap(s::swiftDtoa) , SWIFT_LIB_SUPPORTED); args.emplace_back("yy_double" , wrap(s::yy_double) , YY_DOUBLE_SUPPORTED && std::is_same_v); args.emplace_back("std::to_chars" , wrap(s::std_to_chars) , TO_CHARS_SUPPORTED); - // to_string, snprintf and abseil do not support shortest-length representation + // grisu2 does not round-trip correctly } else { // fixed-length representation - auto&& wrap = make_fixed_adapter; - namespace f = BenchmarkFixedSize; - args.emplace_back("snprintf" , wrap(f::snprintf) , true); - args.emplace_back("abseil" , wrap(f::abseil) , ABSEIL_SUPPORTED); - - // to_string is hard-coded for 6 digits after the decimal point - // args.emplace_back("to_string", BenchmarkFixedSize::to_string, true); - fmt::println("# testing fixed-size output to {} digits", fixed_size); - for (auto &arg : args) - arg.fixedSize = fixed_size; + BenchArgs::initFixedSize(fixed_size); + + namespace f = BenchmarkFixedSize; + args.emplace_back("dragon4" , wrap(f::dragon4) , true , 10); + args.emplace_back("netlib" , wrap(f::netlib) , NETLIB_SUPPORTED , 10); + args.emplace_back("abseil" , wrap(f::abseil) , ABSEIL_SUPPORTED); + args.emplace_back("snprintf" , wrap(f::snprintf) , true); + args.emplace_back("fmt_format" , wrap(f::fmt_format) , true); + args.emplace_back("ryu" , wrap(f::ryu) , std::is_same_v); + args.emplace_back("double_conversion" , wrap(f::double_conversion) , true); + args.emplace_back("std::to_chars" , wrap(f::std_to_chars) , TO_CHARS_SUPPORTED); } if (repeat > 0) { - fmt::println("# forcing repeat count to {}", repeat); - for (auto &arg : args) - arg.testRepeat = repeat; + fmt::println("# forcing repeat count to {}", repeat); + for (auto &arg : args) + arg.testRepeat = repeat; } return args; diff --git a/benchmarks/benchmark.cpp b/benchmarks/benchmark.cpp index 4f386b6..da6a2d0 100644 --- a/benchmarks/benchmark.cpp +++ b/benchmarks/benchmark.cpp @@ -93,7 +93,7 @@ void process(const std::vector> &lines, char buf[100]; std::span bufspan(buf, sizeof(buf)); for (const auto d : lines) - volume += algo.func(d.value, bufspan, algo.fixedSize); + volume += algo.func(d.value, bufspan); return volume; }, algo.testRepeat); } diff --git a/benchmarks/benchutil.h b/benchmarks/benchutil.h index 186b013..523c3c1 100644 --- a/benchmarks/benchutil.h +++ b/benchmarks/benchutil.h @@ -101,7 +101,7 @@ void evaluate_properties_helper(Range&& cases, // representation using the fewest significant digits. // So we use dragonbox, which serves as the reference implementation. const size_t vRef = BenchmarkShortest::dragonbox(d, bufRef); - const size_t vAlgo = algo.func(d, bufAlgo, algo.fixedSize); + const size_t vAlgo = algo.func(d, bufAlgo); std::string_view svRef{bufRef.data(), vRef}, svAlgo{bufAlgo.data(), vAlgo};