Skip to content

Commit 7f3f46a

Browse files
committed
exhaustive 32-bit floats
1 parent 8628ca0 commit 7f3f46a

File tree

6 files changed

+224
-56
lines changed

6 files changed

+224
-56
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@ Serialize strings generated from floats in (0,1):
5959
./build/benchmarks/benchmark
6060
```
6161

62+
## Exhaustive 32-bit check
63+
64+
We also include an exhaustive check of all 32-bit floats, to verify
65+
that we can produce the shortest string representation.
66+
67+
```
68+
./build/benchmarks/exhaustivefloat32
69+
```
70+
6271
## Other existing benchmarks
6372

6473
- [dtoa Benchmark](https://github.com/miloyip/dtoa-benchmark)

benchmarks/CMakeLists.txt

Lines changed: 40 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
add_executable(benchmark
22
benchmark.cpp
3-
ieeeToString.cpp
43
)
5-
4+
add_library(benchmark_deps INTERFACE)
5+
add_library(ieeeToString ieeeToString.cpp)
6+
target_include_directories(ieeeToString PRIVATE ${ryu_SOURCE_DIR})
7+
target_link_libraries(benchmark_deps INTERFACE ieeeToString)
68
include(CheckSourceCompiles)
79
check_source_compiles(CXX "
810
#include <charconv>
@@ -15,7 +17,7 @@ int main(void) {
1517
return 0;
1618
}
1719
" FROM_CHARS_OK)
18-
target_compile_definitions(benchmark PUBLIC FROM_CHARS_SUPPORTED=$<IF:$<BOOL:${FROM_CHARS_OK}>,1,0>)
20+
target_compile_definitions(benchmark_deps INTERFACE FROM_CHARS_SUPPORTED=$<IF:$<BOOL:${FROM_CHARS_OK}>,1,0>)
1921
check_source_compiles(CXX "
2022
#include <charconv>
2123
int main(void) {
@@ -27,7 +29,7 @@ int main(void) {
2729
return 0;
2830
}
2931
" TO_CHARS_OK)
30-
target_compile_definitions(benchmark PUBLIC TO_CHARS_SUPPORTED=$<IF:$<BOOL:${TO_CHARS_OK}>,1,0>)
32+
target_compile_definitions(benchmark_deps INTERFACE TO_CHARS_SUPPORTED=$<IF:$<BOOL:${TO_CHARS_OK}>,1,0>)
3133
if(TO_CHARS_OK)
3234
message(STATUS "std::to_chars with floats is supported")
3335
else(TO_CHARS_OK)
@@ -42,45 +44,55 @@ CPMAddPackage(
4244
NAME fast_float
4345
GITHUB_REPOSITORY "fastfloat/fast_float"
4446
GIT_TAG v8.0.2)
45-
target_link_libraries(benchmark PUBLIC fast_float)
47+
target_link_libraries(benchmark_deps INTERFACE fast_float)
4648
if (NOT WIN32)
47-
target_link_libraries(benchmark PUBLIC netlib)
48-
target_compile_definitions(benchmark PUBLIC NETLIB_SUPPORTED=1)
49+
target_link_libraries(benchmark_deps INTERFACE netlib)
50+
target_compile_definitions(benchmark_deps INTERFACE NETLIB_SUPPORTED=1)
4951
else()
50-
target_compile_definitions(benchmark PUBLIC NETLIB_SUPPORTED=0)
52+
target_compile_definitions(benchmark_deps INTERFACE NETLIB_SUPPORTED=0)
5153
endif()
5254

5355
if (NOT CYGWIN)
54-
target_link_libraries(benchmark PUBLIC absl::str_format)
55-
target_compile_definitions(benchmark PUBLIC ABSEIL_SUPPORTED=1)
56+
target_link_libraries(benchmark_deps INTERFACE absl::str_format)
57+
target_compile_definitions(benchmark_deps INTERFACE ABSEIL_SUPPORTED=1)
5658
else()
57-
target_compile_definitions(benchmark PUBLIC ABSEIL_SUPPORTED=0)
59+
target_compile_definitions(benchmark_deps INTERFACE ABSEIL_SUPPORTED=0)
5860
endif()
5961

6062
if(TARGET errol)
61-
target_link_libraries(benchmark PUBLIC errol)
62-
target_compile_definitions(benchmark PUBLIC ERROL_SUPPORTED=1)
63+
target_link_libraries(benchmark_deps INTERFACE errol)
64+
target_compile_definitions(benchmark_deps INTERFACE ERROL_SUPPORTED=1)
6365
else()
64-
target_compile_definitions(benchmark PUBLIC ERROL_SUPPORTED=0)
66+
target_compile_definitions(benchmark_deps INTERFACE ERROL_SUPPORTED=0)
6567
endif()
6668

67-
target_link_libraries(benchmark PUBLIC fmt)
68-
target_link_libraries(benchmark PUBLIC cxxopts)
69+
target_link_libraries(benchmark_deps INTERFACE fmt)
70+
target_link_libraries(benchmark_deps INTERFACE cxxopts)
6971

70-
target_link_libraries(benchmark PUBLIC yy_double)
71-
target_link_libraries(benchmark PUBLIC double-conversion)
72-
target_link_libraries(benchmark PUBLIC ryu::ryu)
73-
target_link_libraries(benchmark PUBLIC teju)
72+
target_link_libraries(benchmark_deps INTERFACE yy_double)
73+
target_link_libraries(benchmark_deps INTERFACE double-conversion)
74+
target_link_libraries(benchmark_deps INTERFACE ryu::ryu)
75+
target_link_libraries(benchmark_deps INTERFACE teju)
7476
if(teju_has_float128)
75-
target_link_libraries(benchmark PUBLIC teju_boost_multiprecision)
77+
target_link_libraries(benchmark_deps INTERFACE teju_boost_multiprecision)
7678
endif()
77-
target_link_libraries(benchmark PUBLIC dragonbox::dragonbox_to_chars)
78-
target_link_libraries(benchmark PUBLIC dragon_schubfach_lib)
79-
target_link_libraries(benchmark PUBLIC dragon4_lib)
79+
target_link_libraries(benchmark_deps INTERFACE dragonbox::dragonbox_to_chars)
80+
target_link_libraries(benchmark_deps INTERFACE dragon_schubfach_lib)
81+
target_link_libraries(benchmark_deps INTERFACE dragon4_lib)
8082
if(TARGET swift_lib)
81-
target_link_libraries(benchmark PUBLIC swift_lib)
82-
target_compile_definitions(benchmark PUBLIC SWIFT_LIB_SUPPORTED=1)
83+
target_link_libraries(benchmark_deps INTERFACE swift_lib)
84+
target_compile_definitions(benchmark_deps INTERFACE SWIFT_LIB_SUPPORTED=1)
8385
else(TARGET swift_lib)
84-
target_compile_definitions(benchmark PUBLIC SWIFT_LIB_SUPPORTED=0)
86+
target_compile_definitions(benchmark_deps INTERFACE SWIFT_LIB_SUPPORTED=0)
8587
endif(TARGET swift_lib)
86-
target_include_directories(benchmark PUBLIC ${grisu-exact_SOURCE_DIR})
88+
target_include_directories(benchmark_deps INTERFACE ${grisu-exact_SOURCE_DIR})
89+
90+
target_link_libraries(benchmark PUBLIC benchmark_deps)
91+
92+
if(TO_CHARS_OK)
93+
add_executable(exhaustivefloat32
94+
exhaustivefloat32.cpp
95+
)
96+
target_link_libraries(exhaustivefloat32 PUBLIC benchmark_deps)
97+
endif(TO_CHARS_OK)
98+

benchmarks/algorithms.h

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ int abseil(T d, std::span<char>& buffer) {
266266
// absl::StrAppend(&s, d);
267267
// std::copy(s.begin(), s.end(), buffer.begin());
268268
// return size(s);
269-
return absl::SNPrintF(buffer.data(), buffer.size(), "%.17g", d);
269+
return absl::SNPrintF(buffer.data(), buffer.size(), "%g", d);
270270
}
271271

272272
template<arithmetic_float T>
@@ -285,6 +285,32 @@ int std_to_chars(T d, std::span<char>& buffer) {
285285
#endif
286286
}
287287

288+
289+
template <arithmetic_float T>
290+
std::array<BenchArgs<T>, Benchmarks::COUNT> initArgs(bool errol = false) {
291+
std::array<BenchArgs<T>, Benchmarks::COUNT> args;
292+
args[Benchmarks::DRAGON4] = { "dragon4" , Benchmarks::dragon4<T> , true , 10 };
293+
args[Benchmarks::ERROL3] = { "errol3" , Benchmarks::errol3<T> , errol };
294+
args[Benchmarks::TO_STRING] = { "std::to_string" , Benchmarks::to_string<T> , ERROL_SUPPORTED };
295+
args[Benchmarks::FMT_FORMAT] = { "fmt::format" , Benchmarks::fmt_format<T> , true };
296+
args[Benchmarks::NETLIB] = { "netlib" , Benchmarks::netlib<T> , NETLIB_SUPPORTED && std::is_same_v<T, double>, 10 };
297+
args[Benchmarks::SNPRINTF] = { "snprintf" , Benchmarks::snprintf<T> , true };
298+
args[Benchmarks::GRISU2] = { "grisu2" , Benchmarks::grisu2<T> , std::is_same_v<T, double> };
299+
args[Benchmarks::GRISU_EXACT] = { "grisu_exact" , Benchmarks::grisu_exact<T> , true };
300+
args[Benchmarks::SCHUBFACH] = { "schubfach" , Benchmarks::schubfach<T> , true };
301+
args[Benchmarks::DRAGONBOX] = { "dragonbox" , Benchmarks::dragonbox<T> , true };
302+
args[Benchmarks::RYU] = { "ryu" , Benchmarks::ryu<T> , true };
303+
args[Benchmarks::TEJU_JAGUA] = { "teju_jagua" , Benchmarks::teju_jagua<T> , true };
304+
args[Benchmarks::DOUBLE_CONVERSION] = { "double_conversion" , Benchmarks::double_conversion<T> , true };
305+
args[Benchmarks::ABSEIL] = { "abseil" , Benchmarks::abseil<T> , ABSEIL_SUPPORTED };
306+
args[Benchmarks::STD_TO_CHARS] = { "std::to_chars" , Benchmarks::std_to_chars<T> , TO_CHARS_SUPPORTED };
307+
args[Benchmarks::GRISU3] = { "grisu3" , Benchmarks::grisu3<T> , std::is_same_v<T, double> };
308+
args[Benchmarks::SWIFT_DTOA] = { "SwiftDtoa" , Benchmarks::swiftDtoa<T> , SWIFT_LIB_SUPPORTED };
309+
args[Benchmarks::YY_DOUBLE] = { "yy_double" , Benchmarks::yy_double<T> , YY_DOUBLE_SUPPORTED && std::is_same_v<T, double> };
310+
return args;
311+
};
312+
313+
288314
} // namespace Benchmarks
289315

290316
#endif

benchmarks/benchmark.cpp

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ void evaluateProperties(const std::vector<T> &lines,
6060
assert(ptrAlgo == bufAlgo.data() + vAlgo);
6161
assert(ecAlgo == std::errc());
6262
if ((incorrect += (d != dAlgo)) == 1)
63-
fmt::println("\t{:20} mismatch: d = {:.17f}, bufRef = {}, bufAlgo = {}, dAlgo = {:.17f}",
63+
fmt::println("#\t{:20} mismatch: d = {:.17f}, bufRef = {}, bufAlgo = {}, dAlgo = {:.17f}",
6464
algo.name, d, bufRef.data(), bufAlgo.data(), dAlgo);
6565
}
6666
fmt::println("{:20} {:20}", algo.name, incorrect == 0 ? "yes" : "no");
@@ -178,36 +178,13 @@ int main(int argc, char **argv) {
178178
numbers = fileload<double>(filename);
179179
}
180180

181-
auto initArgs = [&](auto type) {
182-
using T = decltype(type);
183-
std::array<BenchArgs<T>, Benchmarks::COUNT> args;
184-
args[Benchmarks::DRAGON4] = { "dragon4" , Benchmarks::dragon4<T> , true , 10 };
185-
args[Benchmarks::ERROL3] = { "errol3" , Benchmarks::errol3<T> , result["errol"].as<bool>() };
186-
args[Benchmarks::TO_STRING] = { "std::to_string" , Benchmarks::to_string<T> , ERROL_SUPPORTED };
187-
args[Benchmarks::FMT_FORMAT] = { "fmt::format" , Benchmarks::fmt_format<T> , true };
188-
args[Benchmarks::NETLIB] = { "netlib" , Benchmarks::netlib<T> , NETLIB_SUPPORTED , 10 };
189-
args[Benchmarks::SNPRINTF] = { "snprintf" , Benchmarks::snprintf<T> , true };
190-
args[Benchmarks::GRISU2] = { "grisu2" , Benchmarks::grisu2<T> , true };
191-
args[Benchmarks::GRISU_EXACT] = { "grisu_exact" , Benchmarks::grisu_exact<T> , true };
192-
args[Benchmarks::SCHUBFACH] = { "schubfach" , Benchmarks::schubfach<T> , true };
193-
args[Benchmarks::DRAGONBOX] = { "dragonbox" , Benchmarks::dragonbox<T> , true };
194-
args[Benchmarks::RYU] = { "ryu" , Benchmarks::ryu<T> , true };
195-
args[Benchmarks::TEJU_JAGUA] = { "teju_jagua" , Benchmarks::teju_jagua<T> , true };
196-
args[Benchmarks::DOUBLE_CONVERSION] = { "double_conversion" , Benchmarks::double_conversion<T> , true };
197-
args[Benchmarks::ABSEIL] = { "abseil" , Benchmarks::abseil<T> , ABSEIL_SUPPORTED };
198-
args[Benchmarks::STD_TO_CHARS] = { "std::to_chars" , Benchmarks::std_to_chars<T> , TO_CHARS_SUPPORTED };
199-
args[Benchmarks::GRISU3] = { "grisu3" , Benchmarks::grisu3<T> , true };
200-
args[Benchmarks::SWIFT_DTOA] = { "SwiftDtoa" , Benchmarks::swiftDtoa<T> , SWIFT_LIB_SUPPORTED };
201-
args[Benchmarks::YY_DOUBLE] = { "yy_double" , Benchmarks::yy_double<T> , YY_DOUBLE_SUPPORTED };
202-
return args;
203-
};
204-
205181
std::variant<std::array<BenchArgs<float>, Benchmarks::COUNT>,
206182
std::array<BenchArgs<double>, Benchmarks::COUNT>> algorithms;
183+
const bool errol = result["errol"].as<bool>();
207184
if (single)
208-
algorithms = initArgs(float{});
185+
algorithms = Benchmarks::initArgs<float>(errol);
209186
else
210-
algorithms = initArgs(double{});
187+
algorithms = Benchmarks::initArgs<double>(errol);
211188

212189
const bool test = result["test"].as<bool>();
213190
std::visit([test](const auto &lines, const auto &args) {

benchmarks/exhaustivefloat32.cpp

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
2+
#include "algorithms.h"
3+
#include "cxxopts.hpp"
4+
#include <bit>
5+
#include <cctype>
6+
#include <cmath>
7+
#include <cstring>
8+
#include <fmt/format.h>
9+
#include <iostream>
10+
#include <string_view>
11+
12+
size_t count_significant_digits(std::string_view num_str) {
13+
size_t count = 0;
14+
bool has_decimal = false;
15+
bool in_exponent = false;
16+
bool leading_zero = true;
17+
18+
for (char c : num_str) {
19+
if (c == '.') {
20+
has_decimal = true;
21+
continue;
22+
}
23+
if (c == 'e' || c == 'E') {
24+
in_exponent = true;
25+
continue;
26+
}
27+
if (std::isdigit(static_cast<unsigned char>(c))) {
28+
if (!in_exponent) {
29+
if (leading_zero && c == '0') {
30+
// Skip leading zeros before decimal
31+
continue;
32+
}
33+
leading_zero = false;
34+
count++;
35+
}
36+
}
37+
}
38+
39+
// Special case: "X.0" should count as 1 digit
40+
if (has_decimal && count > 1) {
41+
auto last_digit_pos = num_str.find_last_not_of("0eE+-");
42+
if (last_digit_pos != std::string_view::npos &&
43+
num_str[last_digit_pos] == '.' && count == 2) {
44+
return 1;
45+
}
46+
}
47+
48+
return count;
49+
}
50+
51+
std::string float_to_hex(float f) {
52+
if (std::isnan(f) || std::isinf(f)) {
53+
return fmt::format("{}", f); // Handle special cases
54+
}
55+
56+
uint32_t bits = std::bit_cast<uint32_t>(f);
57+
int exponent;
58+
float mantissa = std::frexp(f, &exponent); // Get mantissa and exponent
59+
uint32_t mantissa_bits = bits & 0x7FFFFF; // 23-bit mantissa
60+
int exp_bits = (bits >> 23) & 0xFF; // 8-bit exponent
61+
bool sign = bits >> 31; // Sign bit
62+
63+
// Adjust for IEEE 754 representation
64+
if (exp_bits == 0 && mantissa_bits == 0) {
65+
return "0x0p+0"; // Zero case
66+
}
67+
68+
// Convert to hex format
69+
return fmt::format("0x1.{:06x}p{:+d}", mantissa_bits, exponent - 23);
70+
}
71+
void run_exhaustive32(bool errol) {
72+
constexpr auto precision = std::numeric_limits<float>::digits10;
73+
fmt::println("{:20} {:20}", "Algorithm", "Valid shortest serialization");
74+
75+
std::array<Benchmarks::BenchArgs<float>, Benchmarks::COUNT> args;
76+
args = Benchmarks::initArgs<float>(errol);
77+
78+
for (const auto &algo : args) {
79+
if (!algo.used) {
80+
std::cout << "# skipping " << algo.name << std::endl;
81+
continue;
82+
}
83+
bool incorrect = false;
84+
char buf1[100], buf2[100];
85+
std::span<char> bufRef(buf1, sizeof(buf1)), bufAlgo(buf2, sizeof(buf2));
86+
fmt::print("# processing {}", algo.name);
87+
fflush(stdout);
88+
for (uint64_t i = 0; i < (1ULL << 32); ++i) {
89+
if (i % 0x2000000 == 0) {
90+
printf(".");
91+
fflush(stdout);
92+
}
93+
static_assert(sizeof(float) == sizeof(uint32_t));
94+
uint32_t i32(i);
95+
float d;
96+
std::memcpy(&d, &i32, sizeof(float));
97+
if (std::isnan(d) || std::isinf(d))
98+
continue;
99+
// Reference output
100+
const size_t vRef = Benchmarks::std_to_chars(d, bufRef);
101+
const size_t vAlgo = algo.func(d, bufAlgo);
102+
103+
std::string_view svRef{bufRef.data(), vRef};
104+
std::string_view svAlgo{bufAlgo.data(), vAlgo};
105+
106+
auto countRef = count_significant_digits(svRef);
107+
auto countAlgo = count_significant_digits(svAlgo);
108+
if (countRef != countAlgo) {
109+
incorrect = true;
110+
fmt::print(" mismatch: d = {}, bufRef = {}, bufAlgo = {}", float_to_hex(d),
111+
svRef, svAlgo);
112+
fflush(stdout);
113+
break;
114+
}
115+
}
116+
printf("\n");
117+
fmt::println("{:20} {:20}", algo.name, incorrect == 0 ? "yes" : "no");
118+
}
119+
}
120+
121+
cxxopts::Options
122+
options("exhaustivefloat32",
123+
"Verify serialization of all possible float32 values.");
124+
125+
int main(int argc, char **argv) {
126+
try {
127+
options.add_options()(
128+
"e,errol",
129+
"Enable errol3 (current impl. returns invalid values, e.g., for 0).",
130+
cxxopts::value<bool>()->default_value("false"))("h,help",
131+
"Print usage.");
132+
const auto result = options.parse(argc, argv);
133+
134+
if (result["help"].as<bool>()) {
135+
std::cout << options.help() << std::endl;
136+
return EXIT_SUCCESS;
137+
}
138+
run_exhaustive32(result["errol"].as<bool>());
139+
} catch (const std::exception &e) {
140+
std::cout << "error parsing options: " << e.what() << std::endl;
141+
return EXIT_FAILURE;
142+
}
143+
}

dependencies/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ FetchContent_Declare(
6060
GIT_SHALLOW TRUE
6161
)
6262
FetchContent_MakeAvailable(ryu)
63+
set(ryu_SOURCE_DIR ${ryu_SOURCE_DIR} PARENT_SCOPE)
6364

6465
FetchContent_Declare(
6566
grisu-exact

0 commit comments

Comments
 (0)