Why is AsDouble1
much more straightforward than AsDouble0
?
// AsDouble0(unsigned long): # @AsDouble0(unsigned long)
// movq xmm1, rdi
// punpckldq xmm1, xmmword ptr [rip .LCPI0_0] # xmm1 = xmm1[0],mem[0],xmm1[1],mem[1]
// subpd xmm1, xmmword ptr [rip .LCPI0_1]
// movapd xmm0, xmm1
// unpckhpd xmm0, xmm1 # xmm0 = xmm0[1],xmm1[1]
// addsd xmm0, xmm1
// addsd xmm0, xmm0
// ret
double AsDouble0(uint64_t x) { return x * 2.0; }
// AsDouble1(unsigned long): # @AsDouble1(unsigned long)
// shr rdi
// cvtsi2sd xmm0, rdi
// addsd xmm0, xmm0
// ret
double AsDouble1(uint64_t x) { return (x >> 1) * 2.0; }
Code available at: https://godbolt.org/z/dKc6Pe6M1
CodePudding user response:
x86 has an instruction to convert between signed integers and floats. Unsigned integer conversion is (I think) supported by AVX512, which most compilers don't assume by default. If you shift right a uint64_t
once, the sign bit is gone, so you can interpret it as a signed integer and have the same result.
CodePudding user response:
The cvtsi2sd
instruction takes, as its source operand, a signed integer (either 32- or 64-bits wide). However, your functions take unsigned arguments.
Thus, in the first case, the compiler cannot directly use the cvtsi2sd
instruction, because the value in the given argument may not be representable as a same-size signed integer – so it generates code that does the conversion to double
the "long way" (but safely).
However, in your second function, the initial right shift by one bit guarantees that the sign bit will be clear; thus, the resultant value will be identical, whether it is interpreted as signed or unsigned … so the compiler can safely use that (modified) value as the source for the cvtsi2sd
operation.