3

It is said that any decimal number expressed with 15 digits of precision within the range covered by double-precision floats can be uniquely identified with a particular float. But, that if we include more digits of precision (i.e. 16), we may see that two or more decimal numbers correspond to the same float. See https://www.exploringbinary.com/decimal-precision-of-binary-floating-point-numbers/ for more details.

Could someone please provide me with an example of two number with 16 decimal digits precision that map to the same double-precision float?

paxdiablo
  • 854,327
  • 234
  • 1,573
  • 1,953
Jagerber48
  • 488
  • 4
  • 13
  • Changed it again to make sure it's discussing *precision* because technically, `1000000000000000` is a 16-digit decimal number. – paxdiablo Jul 24 '23 at 04:31

3 Answers3

3

In double precision, you have about 52 binary digits of precision (this translates to the roughly 15 decimal digits), that can scale up and down the exponent "curve"(1). This means that a number made up of powers of two will only be differentiable if all of those powers of two are within that distance from each other (on the curve).

So, 250 and 250 + 2-10 will almost certainly become the same number, because the distance between the two terms of that second number differ by 60 on the curve.

Running the following C code confirms this:

#include <math.h>
#include <stdio.h>

int main(void) {
    double d1 = pow(2, 50);
    double d2 = d1 + pow(2, -10);

    if (d1 == d2) puts("Same");
    return 0;
}

In terms of two numbers with 16 decimal digits of precision, that's also doable, as 252 is about 4.5 x 1015. That doesn't quite reach 1016 so what you need to look for is numbers with mostly zeros but a significant one-bit at the top and bottom of the mantissa.

This can be achieved (in one way) with a 9 at the left, and a 1 at the right, so 9.000000000000001 and 9.000000000000002:

#include <stdio.h>

int main(void) {
    double d1 = 9.000000000000001;
    double d1 = 9.000000000000002;

    if (d1 == d2) puts("Same");
    return 0;
}

(1) By curve here, I mean logarithmic scale, where the distance between 210 and 213 is 3.

The way a number is constructed is that the mantissa (or fraction) bits create a base number formed by consecutive powers of two. So the bits 101 could create 22 + 20, or 5 (there's actually an implicit 1 bit at the start in IEEE-754, but we'll ignore that for simplicity).

The exponent bits are then used to scale, or multiply by another power of two, so you could for example, multiply that 5 by 24 = 16 to get 80, or 2-10 = 1/1024 to get 0.0048828125.

And, for completeness, the single sign bit then decides whether it's a positive or negative value. Both that, and the various bit patterns that give you special numbers like NaN and the infinities, are also ignored here because they're not really relevant to the question.

So, in order for a number to be differentiable from another, the most significant and least significant bits must fit within the limited-size mantissa.

See my earlier answer here for more detail on how this all works.

paxdiablo
  • 854,327
  • 234
  • 1,573
  • 1,953
  • I see that 2^50 = 1125899906842624 is a 16 digit integer, but it looks like 2^50 + 2^-10 = 1125899906842624.0009765625 which is a 26 digit decimal. The question is seeking two 16 digit decimal numbers that map to the same float, not just any two decimal numbers that map to the same float. – Jagerber48 Jul 24 '23 at 04:08
  • @Jagerber48: yes, I hadn't read your question closely enough. I've left in the explanation as to how the IEEE-754 representation works because it's important to understanding. I've also added the way in which you can *find* two doubles with 16 decimal digits precision which can't be differentiated, and provided an example. – paxdiablo Jul 24 '23 at 04:27
2

Let us try large 16 decimal digit integers of the form 9 xxx xxx xxx xxx xxx:

#include <stdio.h>

int main() {
  //unsigned long long x0 = 9000000000000000;
  unsigned long long x0 = 1uLL << 53; // 9,007,199,254,740,992
  unsigned long long x1 = 9999999999999999;
  unsigned long long same = 0;
  for (unsigned long long x = x0; x <= x1; x++) {
    double f0 = (double) x;
    double f1 = (double) (x + 1);
    if (f0 == f1) {
      if (same++ == 0) {
        printf("%llu (0x%llX) %llu (0x%llX) both value the value of %f\n", //
            x, x, x + 1, x + 1, f0);
        break;
      }
    }
  }
  printf("Repeats = %llu\n", same);
}

We rapidly find

    9007199254740992 (0x20000000000000) 9007199254740993 (0x20000000000001) both value the value of 9007199254740992.000000
    Repeats = 1

9007199254740993 (0x20000000000001) is a 54-binary digit value. Common double can only exactly encode up to 53-significant binary digit values.


Another way to look at it pigeon hole principle.

Given common double encoding of 52 binary digits per each power-of-2. Between [0.5 ... 1.0), there are 252 or 4,503,599,627,370,496 different double values. Yet in that range there are 5,000,000,000,000,000 different 16 digit decimal values of the form 0.add ddd ddd ddd ddd d (a = [5...9], d = [0-9]), so there is not enough distinct double values to map uniquely to all the 16 decimal digit values of that range. Some decimal values will map to the same double.

#include <string.h>
#include <stdio.h>

int main() {
  //                  1234567890123456
  long long x1 =      9999999999999999; // 0x23 86F2 6FC0 FFFF
  char prior[40] = "0.9999999999999999";
  for (long long x = x1; --x > 0; ) {
    char buf[40];
    sprintf(buf, "0.%16lld", x);
    double value = atof(buf);
    if (value == atof(prior)) {
      printf("%s %s both have value %.20g\n", prior, buf, value);
      break;
    }
    strcpy(prior, buf);
  }
  printf("Done\n");
}

0.9999999999999995 0.9999999999999994 both have value 0.99999999999999944489
Done
chux - Reinstate Monica
  • 143,097
  • 13
  • 135
  • 256
  • 2
    The brute force search is nice. The pigeon hole principle is a nice obvious proof that there MUST be two 16 digit precision decimal numbers that map to the same float. – Jagerber48 Jul 24 '23 at 14:10
1

Shortly after posting this question I found an answer in the comments on the linked article. The 16 digit decimals

8.000 000 000 000 001

and

8.000 000 000 000 002

map to the same float.

In Python, I see

>>> float(8.000000000000001) == float(8.000000000000002)
True
Jagerber48
  • 488
  • 4
  • 13
  • 1
    `nextafter` might be useful know about (in Python's `math` module). also using the `decimal` module to see the full decimal expansion can help to see the actual value represented by a given float – Sam Mason Jul 24 '23 at 10:30