The Curious Case of 3^41

While computers are very good at doing arithmetic quickly, that speed comes with limitations. One such limitation is handling large numbers, especially when many bits of precision are required. While looking into the collatz problem, what first seemed to be a bug, turned out to be something a bit more interesting.

Initial observation:

While computing values related to collatz for x0=27 using a C++ program, the value 341 was needed. A quick check of log2 341 shows that 341 should require 64.983 bits, so it is a little too big to fit into a 64-bit long long.

To get around the limitations of long long, the long double type was considered for its 112-bit significand. Since 341 should fit into 65 bits, the 112-bit significand of long double should be plenty large for it. So, when std::powl(3.0, 41.0) returned an even number, it was most perplexing, as all even numbers have two as a factor.

Question:

Why would std::powl(3.0, 41.0) return an even number when 341 must be odd?

Procedure:

To verify this unexpected result, the following simple C++ program was written.

#include <iomanip>
#include <iostream>

int main(int argc, char **argv) {
    long double ThreeTo41 = std::powl(3.0, 41.0);
    std::cout << std::setprecision(20)
              << "3^41: " << ThreeTo41 << std::endl;
    return 0;
}

When run, the code will output:

3^41: 36472996377170786404

however, using an arbitrary precision calculator, it can be determined that the correct output should be:

3^41: 36472996377170786403

which differs by one in the final digit.

The unexpected result is due to Extended Precision. While long double should be 128-bits wide (i.e., sizeof(long double)==16), it is commonly implemented using an 80-bit register. So when 341 is computed, which should be about 65 binary or 20 decimal digits long, it should easily fit into a long double’s 112-bit significand. Yet the wrong result is reported.  This is due to the 80-bit implementation only having a 64 bit significand.

To better illustrate the cause of this issue, the table below was generated showing the value computed by std::powl, the expected number of bits needed (i.e., log2 3x), and the hexadecimal representation of the result.

x std::powl(3.0, x) log23x 80-bit register
0 110 0 0x3fff800000000000000016
1 310 1.585 0x4000c00000000000000016
2 910 3.17 0x4002900000000000000016
3 2710 4.755 0x4003d80000000000000016
4 8110 6.34 0x4005a20000000000000016
5 24310 7.925 0x4006f30000000000000016
6 72910 9.51 0x4008b64000000000000016
···
32 185302018885184110 50.72 0x4031d2a9fc43c7d0200016
33 555906056655552310 52.3 0x40339dff7d32d5dc180016
34 1667718169966656910 53.89 0x4034ecff3bcc40ca240016
35 5003154509899970710 55.47 0x4036b1bf6cd930979b0016
36 15009463529699912110 57.06 0x4038854f91a2e471b44016
37 45028390589099736310 58.64 0x4039c7f75a7456aa8e6016
38 135085171767299208910 60.23 0x403b95f983d740ffeac816
39 405255515301897626710 61.81 0x403ce0f645c2e17fe02c16
40 1215766545905692880110 63.4 0x403ea8b8b452291fe82116
41 3647299637717078640410 64.98 0x403ffd150e7b3dafdc3216

As can be seen, as x increases, the number of bits needed increases, until 340 uses the very last bit of the 64 bit significand.  So, by 341, the number no longer fits into 64-bits.

To avoid this problem, libgmp had to be used instead of long double.

Conclusion:

While the long double type is 128 bits, and should have a 112 bit significand, it is often implemented using an 80 bit register with only 64 bits of significand.  This can cause unexpected precision issues when computing larger values.

Leave a comment