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.