I have a snippet of code that's present in a VS2017 static library. When linked in a 2017 executable, it works as expected. However, if linking into a 2022 executable, it breaks on the cast from double to uint64_t.
The double
is below -1.0
so the truncated value is outside the value-range of uint64_t
and ISO C doesn't define the behaviour, but MSVC does.
static uint64_t do_something_2(double f1)
{
return (uint64_t)(f1 - 0.5);
}
uint32_t do_something(void)
{
double f1 = -3406302.4613481420;
uint32_t f2 = (uint32_t)do_something_2(f1);
return f2;
}
When linking this into a 2022 executable, f2
is 0xffffffff
, as if from double
to uint64_t
with AVX-512 vcvttsd2usi rax, xmm0
which produces all-ones for out-of-range values. (And then uint64_t to uint32_t truncation to get a 32-bit 0xffffffff
value)
However, if I re-compile the static library using VS2022, I get my expected value of f2 = 4291560994
(0xffcc0622), as if (uint32_t)(uint64_t)double
did get modulo reduction of the integer -3406302
.
(For values that also fit in int64_t
, we can get this result portably and efficiently with (uint64_t)(int64_t)double
, especially in x64 code or with x87+SSE3 fisttp
. But the question is why existing code written with (uint64_t)double
doesn't compile the way I expected when mixing VS versions.)
When I compare the disassembly, I see the 2017-generated code just calls __dtoul3
and nothing more.
The 2022 generated assembly is much more involved with many more calls.
Afterwards, I edited my 2017 makefile by adding /arch=IA32 to disable SSE2 and then recompiled. This resulted in the correct value being computed.
So it seems to be an issue with the SSE2 code generation in VS2017 vs VS2022. However, this blog post on Microsoft seems to suggest that 2022 and 2017 should be compatible: https://devblogs.microsoft.com/cppblog/microsoft-visual-studio-2022-and-floating-point-to-integer-conversions/ It mentions changes to the default semantics for out-of-range FP to integer conversions to match AVX-512 instructions.
Only other things I'll add:
Tried compiling the library with 2019 and linking into the 2022 executable. Same issue as 2017, results in 0xFFFFFFF
The 2017 library linked into a 2017 executable does work.
Any idea why my findings seem to contradict Microsoft's blog post about floating point to integer conversions between 2017 and 2022?
However, if linking into a 2022 executable, it breaks on the cast from double to
uint64_t
.
Undefined behavior (UB)
This is allowed by C.
When a finite value of real floating type is converted to an integer type ..., the fractional part is discarded (i.e., the value is truncated toward zero). If the value of the integral part cannot be represented by the integer type, the behavior is undefined. C17dr § 6.3.1.4 1
Only floating point values in the [-0.99999.... to 18,446,744,073,709,551,616.99999....] range are defined to convert via (uint64_t)
as OP hoped. Since the 3406302.4613481420 - 0.5
is ≤ -1.0, the conversion is UB. @Peter Cordes
double f1 = -3406302.4613481420;
uint64_t y = (uint64_t)(f1 - 0.5); // UB
I'm just curious as to why 2022 isn't compatible with 2017.
OP's code relied on UB and so should not expect the current or prior compiler versions to act consistently. It is not a compiler bug, but an OP one.
Other questionable functionality
f1 - 0.5
is not exact with odd values less than and near 253 and so form an incorrect rounded result.
Further, other values for do_something_2()
may not round as OP might hope. To take further, the exact goal of do_something_2()
is needed.
Alternative
Perform the modulo step with fmod()
and a test.
static uint64_t do_something_2(double f1) {
f1 = fmod(f1, 18446744073709551616.0); // remainder.
if (f1 < 0.0) f1 += 18446744073709551616.0;
return (uint64_t)(f1 - 0.5);
}
This may/may not do exactly what OP wants for all f1
as the overall goal of do_something_2()
remains unstated.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With