7

Why the output of printf("%llu\n", 1ull << n); and printf("%llu\n", 1ull << 64); is different in C++?

Code:

#include <cstdio>

int main()
{
    int n = 64;
    printf("%llu\n", 1ull << n);
    printf("%llu\n", 1ull << 64);
    return 0;
}

Output:

1
0
Vlad from Moscow
  • 301,070
  • 26
  • 186
  • 335
PhoenixGS
  • 71
  • 2

3 Answers3

8

See here:

In any case, if the value of the right operand is negative or is greater or equal to the number of bits in the promoted left operand, the behavior is undefined.

If your left operand is 64 bit and your right operand is 64, this is undefined behavior, and then anything can happen without any guarantee of consistency.

Your compiler should also issue a warning for that, at least it does for me when I try it with GCC or Visual Studio.

Blaze
  • 16,736
  • 2
  • 25
  • 44
  • 1
    Regarding the warning: Same with clang https://godbolt.org/z/QEu9fh – eike Dec 03 '19 at 10:16
  • 1
    Reference to exact standard wording for anyone that needs it: https://eel.is/c++draft/expr.shift#1 – eike Dec 03 '19 at 10:22
5

The reason is that expressions like 1<<64 are compile time constants and are indeed computed by the compiler at compile time. No code to shift anything is emitted.

The expression 1<<64 is evaluated by the compiler as 0, which is plausible and legit since the behavior is actually, as others have pointed out, undefined. The produced assembly for uint64_t i = (uint64_t)1 << 64; is simply to store zero in the variable's location:

QWORD PTR [rbp-16], 0

Now, for a non-compile time value code is emitted. uint64_t i2 = (uint64_t)1 << n; translates to

    mov     rax, QWORD PTR [rbp-8]
    mov     edx, 1
    mov     ecx, eax
    sal     rdx, cl
    mov     rax, rdx
    mov     QWORD PTR [rbp-24], rax

All the boilerplate code before and after the actual SAL shift instruction is just moving the operands in place and moving the result into the variable. The important thing is that the compiler indeed emits code to shift the 1 here. Because shifting by more than 63 is illegal and pointless for 64 bit values Intel processors silently mask the shift value:

REX prefix in the form of REX.W [I must assume that that happens here] promotes operation to 64-bits and sets the mask width for CL to 6 bits.

That is, the processor internally masks n's value of 64/100'0000 with 63/11'1111, resulting in a shift value of 0. The result is, of course, the original 1.

With higher optimization levels the compiler optimizes that instruction away as well because it can infer the value of the non-volatile n, and emits 0 there as well.

Peter - Reinstate Monica
  • 15,048
  • 4
  • 37
  • 62
2

From the C Standard (6.5.7 Bitwise shift operators)

3 The integer promotions are performed on each of the operands. The type of the result is that of the promoted left operand. If the value of the right operand is negative or is greater than or equal to the width of the promoted left operand, the behavior is undefined

So the program has undefined behavior.

The difference of the output can be explained the following way that the compiler generates different object code when an integer constant (literal) is used compared with the code when a variable is used.

Vlad from Moscow
  • 301,070
  • 26
  • 186
  • 335