Thread overview
[Issue 24040] dmd different to ldc and gcc for ldexp(f)
Jul 07, 2023
Iain Buclaw
Jul 08, 2023
Iain Buclaw
Jul 08, 2023
Iain Buclaw
Jul 08, 2023
Iain Buclaw
July 07, 2023
https://issues.dlang.org/show_bug.cgi?id=24040

Iain Buclaw <ibuclaw@gdcproject.org> changed:

           What    |Removed                     |Added
----------------------------------------------------------------------------
                 CC|                            |ibuclaw@gdcproject.org
           See Also|                            |https://issues.dlang.org/sh
                   |                            |ow_bug.cgi?id=21581

--
July 08, 2023
https://issues.dlang.org/show_bug.cgi?id=24040

Iain Buclaw <ibuclaw@gdcproject.org> changed:

           What    |Removed                     |Added
----------------------------------------------------------------------------
           See Also|                            |https://issues.dlang.org/sh
                   |                            |ow_bug.cgi?id=18559

--
July 08, 2023
https://issues.dlang.org/show_bug.cgi?id=24040

--- Comment #1 from Iain Buclaw <ibuclaw@gdcproject.org> ---
Arguably introduced by https://github.com/dlang/dmd/pull/7995 Corresponding druntime PR https://github.com/dlang/druntime/pull/2135

Since druntime/2135, there are now float and double overloads of all core.math intrinsics.

```
float ldexp(float n, int exp);
double ldexp(double n, int exp);
real ldexp(real n, int exp);
```

DMD however only has proper support for the x87 (real) version only - the float and double declarations are no more than aliases.  So the above declarations might as well instead be

```
float ldexp(real n, int exp);
double ldexp(real n, int exp);
real ldexp(real n, int exp);
```

This affects the call to core.math.ldexp, as the first parameter is implicitly cast to real. i.e:

```
core.math.ldexp(cast(real)float(mantissa), -213);
```

As per the spec: https://dlang.org/spec/float.html#fp_intermediate_values

> For floating-point operations and expression intermediate values, a greater precision can be used than the type of the expression. Only the minimum precision is set by the types of the operands, not the maximum. Implementation Note: On Intel x86 machines, for example, it is expected (but not required) that the intermediate calculations be done to the full 80 bits of precision implemented by the hardware.

So the compiler is behaving as expected when it discards the float cast/construction, as `real` has the greater precision over `float(mantissa)`.

If you want to force a maximum precision (float) then you need to use
core.math.toPrec instead.

```
float a = core.math.toPrec!float(core.math.ldexp(real(mantissa), -213));
```

If dmd still doesn't do the right thing, then that is a problem that needs addressing. The compiler has to perform a store and load every time a variable or intermediary value is passed to toPrec.

---

But that's skirting around the sides somewhat. Really, there's lots of intrinsic declarations in core.math to which dmd does not implement - and there's no expectation that it should implement either.

DMD's idea of an intrinsic is that it should result in a single instruction. `real ldexp()` becomes `fscale`, but there is no equivalent instruction for double or float on x86.  Rather, the closest approximation is:

```
fscale;  // ldexp()
...      // Add 4-6 more instructions here to pop+push the result using a
...      // narrower precision (i.e: it should be equivalent to toPrec!T).
```

Instead of these false intrinsics, they should just be regular inline functions, and all special recognition of them removed from the DMD code generator/builtins module.

```
real ldexp(real n, int exp);     /* intrinsic */

/// ditto
pragma(inline, true)
float ldexp(float n, int exp)
{
    return toPrec!float(ldexp(cast(real)n, exp));
}

/// ditto
double ldexp(float n, int exp)
{
    return toPrec!double(ldexp(cast(real)n, exp));
}
```

GDC and LDC can continue to map the float and double overloads to their respective back-end builtins (i.e: libc call), ignoring the function bodies.

---

This could also have been worked around if you used scalbn() instead. ;-)

--
July 08, 2023
https://issues.dlang.org/show_bug.cgi?id=24040

--- Comment #2 from Iain Buclaw <ibuclaw@gdcproject.org> ---
By the way, it has occurred to me that DMD's behaviour is essentially no different to what I'd expect gdc or ldc would emit when compiling with `-ffast-math`.

When giving it a try with gdc, I indeed did see this behaviour in both gdc and g++.

```
$ gdc test.d -o test_d -ffast-math -mfpmath=387 -fno-builtin
$ ./test_d
1
0
$ g++ test.cpp -o test_cpp -ffast-math -mfpmath=387
$ ./test_cpp
1
0
```

As soon as I turn on any optimization levels though, the static "mantissa" is constant propagated and `0 0` is printed at run-time.

(Your C++ test isn't faithful to the D version, adjusted it as follows)

```
#include <cmath>
#include <iostream>

static uint64_t mantissa = 0x8000000000000001UL;

int main() {
    float p =   __builtin_ldexpf((float)mantissa, -213);
    float q =   ldexpf((float)mantissa, -213);
    std::cout << *((unsigned int*)&p) << std::endl;
    std::cout << *((unsigned int*)&q) << std::endl;
}
```

--