1

Why do I get wrong results with log2 (ULONG_MAX) and log2 (ULLONG_MAX)? I was expecting 63 but got 64.

UINT32_MAX == pow(2, 32) - 1

so log2(UINT32_MAX) == 31, (not 32!)

ULLONG_MAX == pow(2, 64) - 1

so I would expect log2(ULLONG_MAX) == 63

but I got 64. Why?

 // 15
 printf ("u_int16: %d\n", (int)log2 (UINT16_MAX));
 // 31
 printf ("u_int:   %d\n", (int)log2 (UINT32_MAX));

 // 64
 printf ("ul_int:  %d\n", (int)log2 (ULONG_MAX));
 // 64
 printf ("ull_int: %d\n", (int)log2 (ULLONG_MAX));
phuclv
  • 37,963
  • 15
  • 156
  • 475
  • 3
    Those numbers probably can't be represented exactly by a double on your system. – Shawn May 16 '21 at 17:06
  • 1
    `log2` is for floating point and can lose precision. For integers this is equivalent to finding the most significant 1 bit. There's no standard C function for this, but you can find some algorithms and compiler-specific approaches here: [Fast computing of log2 for 64-bit integers](https://stackoverflow.com/questions/11376288/fast-computing-of-log2-for-64-bit-integers) – Nate Eldredge May 16 '21 at 18:05
  • 2
    @NateEldredge: (a) `log2` may lose accuracy, but it does not lose precision: The input precision, `double`, is exactly the same as the output precision, `double`. (b) The accuracy of `log2` is not a factor in this question. The change in value occurs before `log2` executes. – Eric Postpischil May 16 '21 at 20:09

3 Answers3

4

TL;DR:

If you want to get the position of the highest 1 bit then you're doing it completely wrong with the slowest and most error-prone way. See the solution at the end


log2() receives a double, but long and long long in your platform have 64 bits of precision, which is far more than double can store, as it's probably IEEE-754 binary64 and has only 53 significant bits. The closest to ULLONG_MAX in double is ULLONG_MAX + 1.0 = 264 and obviously log2(ULLONG_MAX) = log2(264) = 64. You can never get 63 by using double like that

If you want to get the base 2 logarithm of such numbers then you'll need a type with more precision like long double on some platforms, and also a good log2 library (see below for why that matters). On x86 long double is usually 80-bit extended precision with 64 significant bits and can store ULLONG_MAX without problem

#include <stdio.h>
#include <math.h>
#include <quadmath.h>
#include <limits.h>
#include <float.h>

int main()
{
  printf("sizeof(long)        = %zu\n", sizeof(long));
  printf("sizeof(long long)   = %zu\n", sizeof(long long));
  printf("sizeof(double)      = %zu\n", sizeof(double));
  printf("sizeof(long double) = %zu\n", sizeof(long double));
  printf("double      has %d significant bits\n", DBL_MANT_DIG);
  printf("long double has %d significant bits\n", LDBL_MANT_DIG);
  printf("-----------------------------------------------------\n");
  
  printf("ULONG_MAX               = %lu\n", ULONG_MAX);
  printf("ULLONG_MAX              = %llu\n", ULLONG_MAX);
  printf("(double)ULONG_MAX       = %f\n", (double)ULONG_MAX);
  printf("(double)ULLONG_MAX      = %f\n", (double)ULLONG_MAX);
  printf("(long double)ULONG_MAX  = %Lf\n", (long double)ULONG_MAX);
  printf("(long double)ULLONG_MAX = %Lf\n", (long double)ULLONG_MAX);
  printf("-----------------------------------------------------\n");
  
  printf("ul_int (double):\t\t\t%d\n", (int)log2(ULONG_MAX));
  printf("ull_int (double):\t\t\t%d\n", (int)log2(ULLONG_MAX));

  printf("ul_int (long double):\t\t\t%d\n", (int)log2l((long double)ULONG_MAX)); 
  printf("ull_int (long double):\t\t\t%d\n", (int)log2l((long double)ULLONG_MAX));
  printf("ul_int (18446744073709551615.0L):\t%d\n",
          (int)log2l(18446744073709551615.0L));

  printf("ul_int (__float128):\t\t\t%d\n", (int)log2q((__float128)ULONG_MAX));
  printf("ull_int (__float128):\t\t\t%d\n", (int)log2q((__float128)ULLONG_MAX));
  printf("ull_int (18446744073709551615.0q):\t%d\n",
          (int)log2q(18446744073709551615.0q));
}

Demo on Godbolt. Sample output:

sizeof(long)        = 8
sizeof(long long)   = 8
sizeof(double)      = 8
sizeof(long double) = 16
double      has 53 significant bits
long double has 64 significant bits
-----------------------------------------------------
ULONG_MAX               = 18446744073709551615
ULLONG_MAX              = 18446744073709551615
(double)ULONG_MAX       = 18446744073709551616.000000
(double)ULLONG_MAX      = 18446744073709551616.000000
(long double)ULONG_MAX  = 18446744073709551615.000000
(long double)ULLONG_MAX = 18446744073709551615.000000
-----------------------------------------------------
ul_int (double):                    64
ull_int (double):                   64
ul_int (long double):               64
ull_int (long double):              64
ul_int (18446744073709551615.0L):   64
ul_int (__float128):                63
ull_int (__float128):               63
ull_int (18446744073709551615.0q):  63

Notice that ULONG_MAX can't be represented in double precision as I mentioned previously. But also notice that even in long double we get log2l(18446744073709551615.0L) = 64!!! Only __float128 which is libquadmath's IEEE-754 quadruple precision works. Why? Because log and other transcendental functions are very complex and aren't required to be faithfully rounded by IEEE-754, so implementations are allowed to use a much faster algorithm but may return some results with 1ULP error. The result on Godbolt above is for glibc and you need to find some better log2 library as I said above. See

Update:

As commented by chux below, the result might be faithfully rounded in this case but unfortunately the closest long double value to log218446744073709551615 = 63.999999999999999999921791345121706111... is 64.0L

That means you still need higher precision to get the expected output


But probably you're doing it the wrong way. If you just want to get the position of the highest 1 bit then never use log2()!!! It's super slow and is prone to floating-point errors like above. Most architectures have an instruction to get the result in 1 or a few cycles. In C++20 just use std::bit_width(x) or the equivalent

return std::numeric_limits<T>::digits - std::countl_zero(x);

In older C++ versions you can use boost::multiprecision::msb(x), boost::static_log2(x). In C you'll need implementation-specific solutions like

There are also other fast bitwise solutions in

phuclv
  • 37,963
  • 15
  • 156
  • 475
  • I have doubts about "Because log and other transcendental functions aren't required to be faithfully rounded by IEEE-754" applies here. `log2l(18446744073709551615.0L)` is 64.0L and not the next smaller `long double` of `63.9999999999999999 9653...L` because 64.0L is the closer answer - within 0.5 ULP of the correct answer `63.9999999999999999 9992...`. As I see it, the answer is faithfully rounded. – chux - Reinstate Monica May 18 '21 at 11:27
  • @chux-ReinstateMonica yeah that might be the issue. Updated the answer – phuclv May 27 '21 at 17:23
2

log2 is declared double log2(double); it takes a double argument and produces a double result. When log2(ULLONG_MAX) is evaluated, ULLONG_MAX is converted to double.

The format commonly used for double has 53 bits in the significand (the fraction portion of a floating-point representation). Representing ULLONG_MAX requires 63 bits. So ULLONG_MAX cannot be represented in double. Instead, the conversion produces the nearest value that is representable, which is 264.

Then log2 applied to 264 produces 64.

You can see this by printing ULLONG_MAX before and after conversion to double:

printf("%llu\n", ULLONG_MAX);
printf("%.0f\n", (double) ULLONG_MAX);

prints:

18446744073709551615
18446744073709551616
Eric Postpischil
  • 195,579
  • 13
  • 168
  • 312
  • I tried log2l-function. It takes long double values - the same problem. but ULLONG_MAX is much smaller than LDBL_MAX. – top-of-stack May 16 '21 at 17:21
  • 2
    @top-of-stack: The `long double` format is not more precise than `double` in every C implementation. After including ``, you can print `DBL_MANT_DIG` and `LDBL_MANT_DIG` to see how many digits are in the significant of each and `FLT_RADIX` to see what base the digits are in (likely 2). If you get 53 for both `DBL_MANT_DIG` and `LDBL_MANT_DIG`, then they are the same precision in your C implementation. If you get a bigger number for `LDBL_MANT_DIG`, then show exact code that demonstrates your results. – Eric Postpischil May 16 '21 at 17:23
0

Why do I get wrong results with log2(ULONG_MAX) and log2(ULLONG_MAX)? I was expecting 63 but got 64.

Insufficient precision in 2 steps and rounding.


ULLONG_MAX or 18,446,744,073,709,551,615 or 264-1 converts to 18,446,744,073,709,551,616.0 before being when passed to double log2(double). The result is 64.0 which is math correct for log2(264)


I tried log2l-function. It takes long double values - the same problem.

With 80-bit "double extended" extended precision format with its 64-bit precision, ULLONG_MAX coverts to 18,446,744,073,709,551,615.0L when passed to long double log2l(long double). The result is still 64.0L as 64.0L is the best long double answer with that encoding.

64.0                       long double
63.99999999999999999992... math log2(18,446,744,073,709,551,615)
63.99999999999999999653... next smaller long double

To get a log2(ULLONG_MAX) to produce a good result less than 64 (which (int) truncates to 63), the floating point encoding needs:

  • At least 64-bit precision to accommodate the exact conversion of ULLONG_MAX.

  • At least about 69-bit precision to form a rounded answer less than 64.0.

chux - Reinstate Monica
  • 143,097
  • 13
  • 135
  • 256