The long double trouble with MinGW and Windows

Martin LichtMartin Licht
5 min read

Even though most of scientific computing codes are primarly designed for Windows, I take care that they can compiled on Windows environments as well. I use the MinGW ecosystem on Windows, which allows me to work with the GCC and Clang interfaces.

However, a subtle issue appears with long double variables. This is due to the interaction of MinGW and Windows environment, and their respective implementations of long double.

Doubles and long doubles

The C and C++ standards provide three floating-point data types: float, double and long double. the only thing the standards guarantee, very roughly speaking, is that double is at least as large as float, and that long double is at least as large as double.

The sizes of the floating-point data types are implementation defined. Typically, float has at least 4 bytes and double has at least 8 bytes. However, even common implementations disagree on the size of long double.

For example, the type long double may be implemented as the 80-bit extended precision type, which is natively supported on x86 architectures, or as the 128-bit quadruple precision type, often emulated in software. Both GCC and Clang will use these types to implement long double if supported by the processor.

However, long double is a synonym for double in the Windows C/C++ ecoystem, irrespective of what the processors support. Thus, it typically features only the usual 64 bits on Windows.

The complications now arise if those two ecosystems interact, as is the case with MinGW and Windows.

Problem

The following C code illustrates the issue:

#include <stdio.h>
int main() {
    double d = 1.0L / 3.0L;
    printf("double:  %le\n",d);
    long double ld = 1.0L / 3.0L;
    printf("long double: %Le\n",ld);
    return 0;
}

On GNU systems, this works as expected and prints the correct result:

double: 3.333333e-01 long double: 3.333333e-01

However, if we compile this via MinGW on Windows, the program may print something different, such as:

double: 3.333333e-01 long double: 4.206749e-313

What is happening here, and how do we fix this bug?

Causes for the bug

The origin of the incorrect output is not immediately within our program. The floating-point computations are working just fine. Instead, the cause of the problem is in the output routine printf.

Ostensibly, we are doing nothing wrong: the format flags are correct and the arguments have the correct type. However, the printf implementation used is the Microsoft C Runtime Library (MSVCRT). And for that implementation of the C standard library, long double means double.

By contrast, the MinGW C compiler follows the rules of the GCC/Clang ecoystem, where long double is whatever the processor provides.

Indeed, we can confirm that MSVCRT is being linked by either of the following:

  • Run ldd my_program.exe to list dynamic dependencies.
  • Explicitly compile with -Wl,-verbose to see the linker output. You may something similar to
      ld.lld: Loaded libmsvcrt.a(lib64_libucrt_extra_a-ucrt_printf.o) for printf
    

The reason that extended precision has been largely abandoned in the Windows ecosystem is somewhat anecdotal, though developers have commented about it https://forums.codeguru.com/showthread.php?390950-RESOLVED-When-is-80bit-long-double-coming-back. At the time of this writing, this is 20 years in the past. Since MS places great emphasis on backwards compatibility, I would not expect extended precision to return in the Windows C/C++ ecosystem anytime soon.

How to fix the bug?

I would like to point out two alternatives how to circumvent the problem.

  1. MinGW compilation flag One option is using a drop-in replacement that comes with MinGW and replaces the MSVCRT version of printf. To enable that, we define

     #define __USE_MINGW_ANSI_STDIO 1
    

    at the very beginning of the source code, or provide this macro as a compilation option:

     -D__USE_MINGW_ANSI_STDIO=1
    

    The program should now behave correctly.

    This solution has the possible downside that __USE_MINGW_ANSI_STDIO is intended as a purely intrinsic macro and should not be set by the users. Since the macro is not part of the external interface, there is no guarantee that this solution remains stable over time.

  2. Change long double implementation via -mlong-double-64. If the processor belongs to the x86 family, then the compiler option -mlong-double-64 explicitly forces the long doubles to be 64-bit, making it the same as double on Windows. The handling of long double will be compatible with the Windows ABI.

    In particular, this solution, if available, can mitigate similar problems with libraries for who long double equals double.

    Of course, apart from this switch only being available on x86 processors, any source code compiled with that flag will only be compatible with libraries that have 64-bit long doubles. Last but not lest, this switch may or may not defeat the purpose of extended precision in the first place.

Practical conclusions

The practical issue is that any interaction of MinGW-compiled code with the Windows-compiled code will not work properly when long double is involved.

This becomes apparent with simple examples such as the ones using printf in C or the streams library in C++ (which typically uses printf internally). We have focused on the input/output standard libraries of C and C++ in this article, though conclusions can be drawn for other (floating-point heavy) libraries too.

The suggested mitigations above are not flawless but may suffice in some circumstances, in particular, if long doubles are not critically important to the code and if only the input/output standard libraries are of concern.

Lastly, a better solution might be to use a custom input/output library. That, however, is not part of this article.

0
Subscribe to my newsletter

Read articles from Martin Licht directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Martin Licht
Martin Licht