floating-pointprecision

Which single-precision floating point numbers need 9 significant decimal digits for unambiguous decimal represenatation?


I found the following statement in this wikipedia article about single-precision floating point numbers https://en.wikipedia.org/wiki/Single-precision_floating-point_format:

If an IEEE 754 single-precision number is converted to a decimal string with at least 9 significant digits, and then converted back to single-precision representation, the final result must match the original number.

I have tried to find examples of single-precision floating point numbers that actually need 9 significant decimal digits and are not already unambiguous with just 8 significant digits and have not found any, e.g. by printing floating point values in the gdb debugger or by trying out converting different values to single precision in octave, but have not found examples that need more than 8 decimal digits to have a different decimal representation than their direct neighbor floating point values.

The question is, are there actually values of single-precision (32 bit) floating point values that need 9 decimal digits, or is this just a safe upper boundary that is never needed. Could you give an example for a single-precision floating point value that when converted to only 8 significant decimal digits and then converted back to the binary floating-point representation, has a different value than the original float.


Solution

  • 32-bit floating point numbers are stored in 32 bits, which means there can not be much more than approximately 4 billion different values. Computers are fast enough to iterate through all numbers, therefore, a brute-force search for 32-bit floating point numbers can automate this in acceptable time, and test for all possible numbers if a conversion to string with only 8 significant decimal digits plus the inverse conversion back from string to single-precision floating point representation alters the value.

    The following short C++ program does this for all positive floating point values:

    #include <cstdio>
    #include <cmath>
    #include <limits>
    #include <cinttypes>
    
    int main(int argc, char**argv) {
      // Test if conversion with /precision/ significant decimal digit is enough
      int precision = 8;
    
      // Can override precision = 8 with a command line parameter
      if (argc > 1) {
        precision = strtol(argv[1], nullptr, 0);
        if (precision < 1 || precision > 50) {
          printf("Error: precision should be between 1 and 50, got %d.\n",
                 precision);
          exit(1);
        }
      }
    
      // Buffer length of character buffers to store string representations of
      // floating point numbers with /precision/ significant digits.  /buflen/ is
      // larger than /precision/ because it also needs to store leading zeros,
      // decimal point, sign, scientific notation exponents, and terminating \0.
      const int buflen = precision + 10;
    
      // storage for current number converted to string with 8 decimal digits
      char str[buflen] = "";
    
      // shorthands for maxfloat and infinity
      const float maxfloat = std::numeric_limits<float>::max();
      const float inf = std::numeric_limits<float>::infinity();
    
      // Count the number of times where /precision/ was not sufficient
      uint64_t num_clashes_found = 0;
    
      // Count all tested floats
      uint64_t num_floats_tested = 0;
    
      // loop over all positive single precision floating point numbers
      for (float f = 0.0f;               // start with zero
           f <= maxfloat;                // iterate up to and including maxfloat
           ++num_floats_tested,          // count the number of all tested floats
           f = nextafterf(f, inf))       // increment f to next larger float value 
      {
        // convert number to string with /precision/ significant decimal digits
        int numprintedchars = snprintf(str, buflen, "%.*g", precision, f);
    
        // If string buffer is not long enough to store number as string with
        // /precision/ significant digits, then print warning and terminate program
        if (numprintedchars >= buflen) {
          printf("Buffer length %d is not enough to store \"%.*g\", should"
                 " be at least %d\n", buflen, precision, f, numprintedchars+1);
          exit(1);
        }
    
        // convert the string back to float
        float float_from_string = strtof(str,nullptr);
    
        // Compare the value
        if (f != float_from_string) {
          printf("%.*g converts to \"%s\" which reads back as %.*g.\n",
                 precision+1, f, str, precision+1, float_from_string);
          ++num_clashes_found;
        }
      }
      printf("Found %" PRIu64" clashes when using %d significant decimal digits.\n",
             num_clashes_found, precision);
      printf("Total number of tested floats is %" PRIu64", i.e. with %d significant"
             " decimal digits, we get clashes in %g%% of all numbers.\n",
             num_floats_tested, precision,
             100.0 / num_floats_tested * num_clashes_found);
      return 0;
    }
    

    This program needs about 20 minutes to iterate through all positive single-precision floating-point numbers.

    One example number that it finds is 0.111294314f. When converted to a decimal string with 8 significant digits, then the result is "0.11129431". The next smaller single-precision floating point number is 0.111294307f, which has the same decimal representation when converted to string with only 8 significant digits.

    Altogether, the program counts that there are about 2.14 billion positive floating point numbers, but only about 32 million of these need 9 significant decimal digits for unambiguous representation. This corresponds to about 1.5% of all numbers that need the 9 digits, which explains why manual testing is somewhat unlikely to find them:

    It is clear that one would manually test floating point values whose decimal representations start with digit 1, because for these you need one more significant decimal digit for the leading 1 compared to the preceding values of very similar value that start with digit 9. However, there are also powers of 10 for which no floating point value that converts to decimal 1.xxx * 10^yy exists that actually needs 9 significant digit. These powers of 10 where 8 significant digits are always sufficient are (exponents of 10 are given, named yy above): -34, -31, -21, -18, -15, -12, -09, -06, -05, -03, +00, +07, +08, +10, +13, +16, +19, +22, +25, +28. If one happens to manually test values near any of these powers of 10, no positive results can be found. This includes 10^0, i.e. values near 1.0, which is probably the most likely place for humans to start a manual search.