Skip to content

Commit 06f3e27

Browse files
authored
Merge pull request #383 from redis-performance/pr/parallel-exhaustive
Parallelize the exhaustive float32 sweeps across hardware threads (~75-88x)
2 parents ed86132 + b642d92 commit 06f3e27

4 files changed

Lines changed: 184 additions & 125 deletions

File tree

tests/CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ cmake_minimum_required(VERSION 3.11 FATAL_ERROR)
44

55
include(FetchContent)
66

7+
# Some tests (the exhaustive sweeps) parallelize across std::thread.
8+
set(THREADS_PREFER_PTHREAD_FLAG ON)
9+
find_package(Threads REQUIRED)
10+
711
option(SYSTEM_DOCTEST "Use system copy of doctest" OFF)
812
option(FASTFLOAT_SUPPLEMENTAL_TESTS "Run supplemental tests" ON)
913

@@ -49,6 +53,7 @@ function(fast_float_add_cpp_test TEST_NAME)
4953
target_compile_options(${TEST_NAME} PUBLIC -Wsign-compare -Wshadow -Wwrite-strings -Wpointer-arith -Winit-self -Wconversion -Wsign-conversion)
5054
endif()
5155
target_link_libraries(${TEST_NAME} PUBLIC fast_float supplemental-data)
56+
target_link_libraries(${TEST_NAME} PUBLIC Threads::Threads)
5257
if (NOT SYSTEM_DOCTEST)
5358
target_link_libraries(${TEST_NAME} PUBLIC doctest)
5459
else ()

tests/exhaustive32.cpp

Lines changed: 51 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,54 +8,68 @@
88
#include <iostream>
99
#include <limits>
1010
#include <system_error>
11+
#include <thread>
12+
#include <vector>
1113

1214
template <typename T> char *to_string(T d, char *buffer) {
1315
auto written = std::snprintf(buffer, 64, "%.*e",
1416
std::numeric_limits<T>::max_digits10 - 1, d);
1517
return buffer + written;
1618
}
1719

18-
void allvalues() {
20+
// Checks a single 32-bit word (interpreted as a float); aborts on a mismatch.
21+
void check_word(uint32_t word) {
1922
char buffer[64];
20-
for (uint64_t w = 0; w <= 0xFFFFFFFF; w++) {
21-
float v;
22-
if ((w % 1048576) == 0) {
23-
std::cout << ".";
24-
std::cout.flush();
23+
float v;
24+
memcpy(&v, &word, sizeof(v));
25+
26+
char const *string_end = to_string(v, buffer);
27+
float result_value;
28+
auto result = fast_float::from_chars(buffer, string_end, result_value);
29+
// Starting with version 4.0 for fast_float, we return result_out_of_range
30+
// if the value is either too small (too close to zero) or too large
31+
// (effectively infinity). So std::errc::result_out_of_range is normal for
32+
// well-formed input strings.
33+
if (result.ec != std::errc() && result.ec != std::errc::result_out_of_range) {
34+
std::cerr << "parsing error ? " << buffer << std::endl;
35+
abort();
36+
}
37+
if (std::isnan(v)) {
38+
if (!std::isnan(result_value)) {
39+
std::cerr << "not nan" << buffer << std::endl;
40+
abort();
2541
}
26-
uint32_t word = uint32_t(w);
27-
memcpy(&v, &word, sizeof(v));
42+
} else if (copysign(1, result_value) != copysign(1, v)) {
43+
std::cerr << "I got " << std::hexfloat << result_value
44+
<< " but I was expecting " << v << std::endl;
45+
abort();
46+
} else if (result_value != v) {
47+
std::cerr << "no match ? " << buffer << std::endl;
48+
std::cout << "started with " << std::hexfloat << v << std::endl;
49+
std::cout << "got back " << std::hexfloat << result_value << std::endl;
50+
std::cout << std::dec;
51+
abort();
52+
}
53+
}
2854

29-
{
30-
char const *string_end = to_string(v, buffer);
31-
float result_value;
32-
auto result = fast_float::from_chars(buffer, string_end, result_value);
33-
// Starting with version 4.0 for fast_float, we return result_out_of_range
34-
// if the value is either too small (too close to zero) or too large
35-
// (effectively infinity). So std::errc::result_out_of_range is normal for
36-
// well-formed input strings.
37-
if (result.ec != std::errc() &&
38-
result.ec != std::errc::result_out_of_range) {
39-
std::cerr << "parsing error ? " << buffer << std::endl;
40-
abort();
41-
}
42-
if (std::isnan(v)) {
43-
if (!std::isnan(result_value)) {
44-
std::cerr << "not nan" << buffer << std::endl;
45-
abort();
46-
}
47-
} else if (copysign(1, result_value) != copysign(1, v)) {
48-
std::cerr << "I got " << std::hexfloat << result_value
49-
<< " but I was expecting " << v << std::endl;
50-
abort();
51-
} else if (result_value != v) {
52-
std::cerr << "no match ? " << buffer << std::endl;
53-
std::cout << "started with " << std::hexfloat << v << std::endl;
54-
std::cout << "got back " << std::hexfloat << result_value << std::endl;
55-
std::cout << std::dec;
56-
abort();
55+
// Sweeps the whole 2^32 float space, split across hardware threads (the values
56+
// are independent); check_word() aborts on the first mismatch.
57+
void allvalues() {
58+
unsigned int nthreads = std::thread::hardware_concurrency();
59+
if (nthreads == 0) {
60+
nthreads = 1;
61+
}
62+
std::vector<std::thread> workers;
63+
workers.reserve(nthreads);
64+
for (unsigned int t = 0; t < nthreads; t++) {
65+
workers.emplace_back([t, nthreads]() {
66+
for (uint64_t w = t; w <= 0xFFFFFFFF; w += nthreads) {
67+
check_word(uint32_t(w));
5768
}
58-
}
69+
});
70+
}
71+
for (std::thread &worker : workers) {
72+
worker.join();
5973
}
6074
std::cout << std::endl;
6175
}

tests/exhaustive32_64.cpp

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11

22
#include "fast_float/fast_float.h"
33

4+
#include <atomic>
45
#include <cassert>
56
#include <cmath>
67
#include <cstdio>
@@ -9,6 +10,8 @@
910
#include <limits>
1011
#include <string>
1112
#include <system_error>
13+
#include <thread>
14+
#include <vector>
1215

1316
template <typename T> char *to_string(T d, char *buffer) {
1417
auto written = std::snprintf(buffer, 64, "%.*e",
@@ -45,25 +48,38 @@ bool basic_test_64bit(std::string vals, double val) {
4548
return true;
4649
}
4750

51+
// Sweeps the whole 2^32 float space (widened to double), split across hardware
52+
// threads (the values are independent); stops at the first mismatch.
4853
void all_32bit_values() {
49-
char buffer[64];
50-
for (uint64_t w = 0; w <= 0xFFFFFFFF; w++) {
51-
float v32;
52-
if ((w % 1048576) == 0) {
53-
std::cout << ".";
54-
std::cout.flush();
55-
}
56-
uint32_t word = uint32_t(w);
57-
memcpy(&v32, &word, sizeof(v32));
58-
double v = v32;
54+
unsigned int nthreads = std::thread::hardware_concurrency();
55+
if (nthreads == 0) {
56+
nthreads = 1;
57+
}
58+
std::atomic<bool> ok{true};
59+
std::vector<std::thread> workers;
60+
workers.reserve(nthreads);
61+
for (unsigned int t = 0; t < nthreads; t++) {
62+
workers.emplace_back([t, nthreads, &ok]() {
63+
char buffer[64];
64+
for (uint64_t w = t;
65+
w <= 0xFFFFFFFF && ok.load(std::memory_order_relaxed);
66+
w += nthreads) {
67+
float v32;
68+
uint32_t word = uint32_t(w);
69+
memcpy(&v32, &word, sizeof(v32));
70+
double v = v32;
5971

60-
{
61-
char const *string_end = to_string(v, buffer);
62-
std::string s(buffer, size_t(string_end - buffer));
63-
if (!basic_test_64bit(s, v)) {
64-
return;
72+
char const *string_end = to_string(v, buffer);
73+
std::string s(buffer, size_t(string_end - buffer));
74+
if (!basic_test_64bit(s, v)) {
75+
ok.store(false, std::memory_order_relaxed);
76+
return;
77+
}
6578
}
66-
}
79+
});
80+
}
81+
for (std::thread &worker : workers) {
82+
worker.join();
6783
}
6884
std::cout << std::endl;
6985
}

tests/exhaustive32_midpoint.cpp

Lines changed: 96 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
#include "fast_float/fast_float.h"
22

3+
#include <atomic>
34
#include <cassert>
45
#include <cmath>
56
#include <cstdio>
67
#include <ios>
78
#include <iostream>
89
#include <limits>
910
#include <stdexcept>
11+
#include <thread>
12+
#include <vector>
1013

1114
#if defined(__CYGWIN__) || defined(__MINGW32__) || defined(__MINGW64__)
1215
// Anything at all that is related to cygwin, msys and so forth will
@@ -74,86 +77,107 @@ void strtof_from_string(char const *st, float &d) {
7477
}
7578
}
7679

77-
bool allvalues() {
80+
// Checks a single 32-bit word (interpreted as a float). Returns true if the
81+
// parser agrees with the reference, false (after logging) on a mismatch.
82+
bool check_word(uint32_t word) {
7883
char buffer[64];
79-
for (uint64_t w = 0; w <= 0xFFFFFFFF; w++) {
80-
float v;
81-
if ((w % 1048576) == 0) {
82-
std::cout << ".";
83-
std::cout.flush();
84-
}
85-
uint32_t word = uint32_t(w);
86-
memcpy(&v, &word, sizeof(v));
87-
if (std::isfinite(v)) {
88-
float nextf = std::nextafterf(v, INFINITY);
89-
if (copysign(1, v) != copysign(1, nextf)) {
90-
continue;
91-
}
92-
if (!std::isfinite(nextf)) {
93-
continue;
94-
}
95-
double v1{v};
96-
assert(float(v1) == v);
97-
double v2{nextf};
98-
assert(float(v2) == nextf);
99-
double midv{v1 + (v2 - v1) / 2};
100-
float expected_midv = float(midv);
84+
float v;
85+
memcpy(&v, &word, sizeof(v));
86+
if (!std::isfinite(v)) {
87+
return true;
88+
}
89+
float nextf = std::nextafterf(v, INFINITY);
90+
if (copysign(1, v) != copysign(1, nextf)) {
91+
return true;
92+
}
93+
if (!std::isfinite(nextf)) {
94+
return true;
95+
}
96+
double v1{v};
97+
assert(float(v1) == v);
98+
double v2{nextf};
99+
assert(float(v2) == nextf);
100+
double midv{v1 + (v2 - v1) / 2};
101+
float expected_midv = float(midv);
101102

102-
char const *string_end = to_string(midv, buffer);
103-
float str_answer;
104-
strtof_from_string(buffer, str_answer);
103+
char const *string_end = to_string(midv, buffer);
104+
float str_answer;
105+
strtof_from_string(buffer, str_answer);
105106

106-
float result_value;
107-
auto result = fast_float::from_chars(buffer, string_end, result_value);
108-
// Starting with version 4.0 for fast_float, we return result_out_of_range
109-
// if the value is either too small (too close to zero) or too large
110-
// (effectively infinity). So std::errc::result_out_of_range is normal for
111-
// well-formed input strings.
112-
if (result.ec != std::errc() &&
113-
result.ec != std::errc::result_out_of_range) {
114-
std::cerr << "parsing error ? " << buffer << std::endl;
115-
return false;
116-
}
117-
if (std::isnan(v)) {
118-
if (!std::isnan(result_value)) {
119-
std::cerr << "not nan" << buffer << std::endl;
120-
std::cerr << "v " << std::hexfloat << v << std::endl;
121-
std::cerr << "v2 " << std::hexfloat << v2 << std::endl;
122-
std::cerr << "midv " << std::hexfloat << midv << std::endl;
123-
std::cerr << "expected_midv " << std::hexfloat << expected_midv
124-
<< std::endl;
125-
return false;
126-
}
127-
} else if (copysign(1, result_value) != copysign(1, v)) {
128-
std::cerr << buffer << std::endl;
129-
std::cerr << "v " << std::hexfloat << v << std::endl;
130-
std::cerr << "v2 " << std::hexfloat << v2 << std::endl;
131-
std::cerr << "midv " << std::hexfloat << midv << std::endl;
132-
std::cerr << "expected_midv " << std::hexfloat << expected_midv
133-
<< std::endl;
134-
std::cerr << "I got " << std::hexfloat << result_value
135-
<< " but I was expecting " << v << std::endl;
136-
return false;
137-
} else if (result_value != str_answer) {
138-
std::cerr << "no match ? " << buffer << std::endl;
139-
std::cerr << "v " << std::hexfloat << v << std::endl;
140-
std::cerr << "v2 " << std::hexfloat << v2 << std::endl;
141-
std::cerr << "midv " << std::hexfloat << midv << std::endl;
142-
std::cerr << "expected_midv " << std::hexfloat << expected_midv
143-
<< std::endl;
144-
std::cout << "started with " << std::hexfloat << midv << std::endl;
145-
std::cout << "round down to " << std::hexfloat << str_answer
146-
<< std::endl;
147-
std::cout << "got back " << std::hexfloat << result_value << std::endl;
148-
std::cout << std::dec;
149-
return false;
150-
}
107+
float result_value;
108+
auto result = fast_float::from_chars(buffer, string_end, result_value);
109+
// Starting with version 4.0 for fast_float, we return result_out_of_range
110+
// if the value is either too small (too close to zero) or too large
111+
// (effectively infinity). So std::errc::result_out_of_range is normal for
112+
// well-formed input strings.
113+
if (result.ec != std::errc() && result.ec != std::errc::result_out_of_range) {
114+
std::cerr << "parsing error ? " << buffer << std::endl;
115+
return false;
116+
}
117+
if (std::isnan(v)) {
118+
if (!std::isnan(result_value)) {
119+
std::cerr << "not nan" << buffer << std::endl;
120+
std::cerr << "v " << std::hexfloat << v << std::endl;
121+
std::cerr << "v2 " << std::hexfloat << v2 << std::endl;
122+
std::cerr << "midv " << std::hexfloat << midv << std::endl;
123+
std::cerr << "expected_midv " << std::hexfloat << expected_midv
124+
<< std::endl;
125+
return false;
151126
}
127+
} else if (copysign(1, result_value) != copysign(1, v)) {
128+
std::cerr << buffer << std::endl;
129+
std::cerr << "v " << std::hexfloat << v << std::endl;
130+
std::cerr << "v2 " << std::hexfloat << v2 << std::endl;
131+
std::cerr << "midv " << std::hexfloat << midv << std::endl;
132+
std::cerr << "expected_midv " << std::hexfloat << expected_midv
133+
<< std::endl;
134+
std::cerr << "I got " << std::hexfloat << result_value
135+
<< " but I was expecting " << v << std::endl;
136+
return false;
137+
} else if (result_value != str_answer) {
138+
std::cerr << "no match ? " << buffer << std::endl;
139+
std::cerr << "v " << std::hexfloat << v << std::endl;
140+
std::cerr << "v2 " << std::hexfloat << v2 << std::endl;
141+
std::cerr << "midv " << std::hexfloat << midv << std::endl;
142+
std::cerr << "expected_midv " << std::hexfloat << expected_midv
143+
<< std::endl;
144+
std::cout << "started with " << std::hexfloat << midv << std::endl;
145+
std::cout << "round down to " << std::hexfloat << str_answer << std::endl;
146+
std::cout << "got back " << std::hexfloat << result_value << std::endl;
147+
std::cout << std::dec;
148+
return false;
152149
}
153-
std::cout << std::endl;
154150
return true;
155151
}
156152

153+
// Sweeps the whole 2^32 float space, split across hardware threads (the values
154+
// are independent). Returns false as soon as any word mismatches.
155+
bool allvalues() {
156+
unsigned int nthreads = std::thread::hardware_concurrency();
157+
if (nthreads == 0) {
158+
nthreads = 1;
159+
}
160+
std::atomic<bool> ok{true};
161+
std::vector<std::thread> workers;
162+
workers.reserve(nthreads);
163+
for (unsigned int t = 0; t < nthreads; t++) {
164+
workers.emplace_back([t, nthreads, &ok]() {
165+
for (uint64_t w = t;
166+
w <= 0xFFFFFFFF && ok.load(std::memory_order_relaxed);
167+
w += nthreads) {
168+
if (!check_word(uint32_t(w))) {
169+
ok.store(false, std::memory_order_relaxed);
170+
return;
171+
}
172+
}
173+
});
174+
}
175+
for (std::thread &worker : workers) {
176+
worker.join();
177+
}
178+
return ok.load();
179+
}
180+
157181
inline void Assert(bool Assertion) {
158182
#if defined(__CYGWIN__) || defined(__MINGW32__) || defined(__MINGW64__) || \
159183
defined(sun) || defined(__sun)

0 commit comments

Comments
 (0)