tests: parallelize exhaustive32 and exhaustive32_64 sweeps too

Same std::thread split as exhaustive32_midpoint; preserves each test's existing
failure behavior (abort for exhaustive32, stop-flag for exhaustive32_64).
diff --git a/tests/exhaustive32.cpp b/tests/exhaustive32.cpp
index 5e7de2d..455d8af 100644
--- a/tests/exhaustive32.cpp
+++ b/tests/exhaustive32.cpp
@@ -8,6 +8,8 @@
 #include <iostream>
 #include <limits>
 #include <system_error>
+#include <thread>
+#include <vector>
 
 template <typename T> char *to_string(T d, char *buffer) {
   auto written = std::snprintf(buffer, 64, "%.*e",
@@ -15,47 +17,59 @@
   return buffer + written;
 }
 
-void allvalues() {
+// Checks a single 32-bit word (interpreted as a float); aborts on a mismatch.
+void check_word(uint32_t word) {
   char buffer[64];
-  for (uint64_t w = 0; w <= 0xFFFFFFFF; w++) {
-    float v;
-    if ((w % 1048576) == 0) {
-      std::cout << ".";
-      std::cout.flush();
-    }
-    uint32_t word = uint32_t(w);
-    memcpy(&v, &word, sizeof(v));
+  float v;
+  memcpy(&v, &word, sizeof(v));
 
-    {
-      char const *string_end = to_string(v, buffer);
-      float result_value;
-      auto result = fast_float::from_chars(buffer, string_end, result_value);
-      // Starting with version 4.0 for fast_float, we return result_out_of_range
-      // if the value is either too small (too close to zero) or too large
-      // (effectively infinity). So std::errc::result_out_of_range is normal for
-      // well-formed input strings.
-      if (result.ec != std::errc() &&
-          result.ec != std::errc::result_out_of_range) {
-        std::cerr << "parsing error ? " << buffer << std::endl;
-        abort();
-      }
-      if (std::isnan(v)) {
-        if (!std::isnan(result_value)) {
-          std::cerr << "not nan" << buffer << std::endl;
-          abort();
-        }
-      } else if (copysign(1, result_value) != copysign(1, v)) {
-        std::cerr << "I got " << std::hexfloat << result_value
-                  << " but I was expecting " << v << std::endl;
-        abort();
-      } else if (result_value != v) {
-        std::cerr << "no match ? " << buffer << std::endl;
-        std::cout << "started with " << std::hexfloat << v << std::endl;
-        std::cout << "got back " << std::hexfloat << result_value << std::endl;
-        std::cout << std::dec;
-        abort();
-      }
+  char const *string_end = to_string(v, buffer);
+  float result_value;
+  auto result = fast_float::from_chars(buffer, string_end, result_value);
+  // Starting with version 4.0 for fast_float, we return result_out_of_range
+  // if the value is either too small (too close to zero) or too large
+  // (effectively infinity). So std::errc::result_out_of_range is normal for
+  // well-formed input strings.
+  if (result.ec != std::errc() && result.ec != std::errc::result_out_of_range) {
+    std::cerr << "parsing error ? " << buffer << std::endl;
+    abort();
+  }
+  if (std::isnan(v)) {
+    if (!std::isnan(result_value)) {
+      std::cerr << "not nan" << buffer << std::endl;
+      abort();
     }
+  } else if (copysign(1, result_value) != copysign(1, v)) {
+    std::cerr << "I got " << std::hexfloat << result_value
+              << " but I was expecting " << v << std::endl;
+    abort();
+  } else if (result_value != v) {
+    std::cerr << "no match ? " << buffer << std::endl;
+    std::cout << "started with " << std::hexfloat << v << std::endl;
+    std::cout << "got back " << std::hexfloat << result_value << std::endl;
+    std::cout << std::dec;
+    abort();
+  }
+}
+
+// Sweeps the whole 2^32 float space, split across hardware threads (the values
+// are independent); check_word() aborts on the first mismatch.
+void allvalues() {
+  unsigned int nthreads = std::thread::hardware_concurrency();
+  if (nthreads == 0) {
+    nthreads = 1;
+  }
+  std::vector<std::thread> workers;
+  workers.reserve(nthreads);
+  for (unsigned int t = 0; t < nthreads; t++) {
+    workers.emplace_back([t, nthreads]() {
+      for (uint64_t w = t; w <= 0xFFFFFFFF; w += nthreads) {
+        check_word(uint32_t(w));
+      }
+    });
+  }
+  for (std::thread &worker : workers) {
+    worker.join();
   }
   std::cout << std::endl;
 }
diff --git a/tests/exhaustive32_64.cpp b/tests/exhaustive32_64.cpp
index c453f28..b520352 100644
--- a/tests/exhaustive32_64.cpp
+++ b/tests/exhaustive32_64.cpp
@@ -1,6 +1,7 @@
 
 #include "fast_float/fast_float.h"
 
+#include <atomic>
 #include <cassert>
 #include <cmath>
 #include <cstdio>
@@ -9,6 +10,8 @@
 #include <limits>
 #include <string>
 #include <system_error>
+#include <thread>
+#include <vector>
 
 template <typename T> char *to_string(T d, char *buffer) {
   auto written = std::snprintf(buffer, 64, "%.*e",
@@ -45,25 +48,38 @@
   return true;
 }
 
+// Sweeps the whole 2^32 float space (widened to double), split across hardware
+// threads (the values are independent); stops at the first mismatch.
 void all_32bit_values() {
-  char buffer[64];
-  for (uint64_t w = 0; w <= 0xFFFFFFFF; w++) {
-    float v32;
-    if ((w % 1048576) == 0) {
-      std::cout << ".";
-      std::cout.flush();
-    }
-    uint32_t word = uint32_t(w);
-    memcpy(&v32, &word, sizeof(v32));
-    double v = v32;
+  unsigned int nthreads = std::thread::hardware_concurrency();
+  if (nthreads == 0) {
+    nthreads = 1;
+  }
+  std::atomic<bool> ok{true};
+  std::vector<std::thread> workers;
+  workers.reserve(nthreads);
+  for (unsigned int t = 0; t < nthreads; t++) {
+    workers.emplace_back([t, nthreads, &ok]() {
+      char buffer[64];
+      for (uint64_t w = t;
+           w <= 0xFFFFFFFF && ok.load(std::memory_order_relaxed);
+           w += nthreads) {
+        float v32;
+        uint32_t word = uint32_t(w);
+        memcpy(&v32, &word, sizeof(v32));
+        double v = v32;
 
-    {
-      char const *string_end = to_string(v, buffer);
-      std::string s(buffer, size_t(string_end - buffer));
-      if (!basic_test_64bit(s, v)) {
-        return;
+        char const *string_end = to_string(v, buffer);
+        std::string s(buffer, size_t(string_end - buffer));
+        if (!basic_test_64bit(s, v)) {
+          ok.store(false, std::memory_order_relaxed);
+          return;
+        }
       }
-    }
+    });
+  }
+  for (std::thread &worker : workers) {
+    worker.join();
   }
   std::cout << std::endl;
 }