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.