As developers we probably all know that floating point precision can be an issue 1. It can haunt us in various ways.
Generally when we talk about precision, though, we probably don’t have in mind printf
as the first thing. This blog post is about a particular change from Visual Studio 2015, which caused some hassle — and how to work around it. It’s more about the formatting than actual precision, but the first thing that comes to mind here would be precision, which is why I chose it for the title.
It is the issue also presented in this forum thread and the relevant excerpt from the change announcement on the VS blog reads:
Floating Point Formatting and Parsing Correctness: We have implemented new floating point formatting and parsing algorithms to improve correctness. This change affects the
printf
andscanf
families of functions, as well as functions likestrtod
.The old formatting algorithms would generate only a limited number of digits, then would fill the remaining decimal places with zero. This is usually good enough to generate strings that will round-trip back to the original floating point value, but it’s not great if you want the exact value (or the closest decimal representation thereof). The new formatting algorithms generate as many digits as are required to represent the value (or to fill the specified precision). As an example of the improvement; consider the results when printing a large power of two:
printf(“%.0f\n”, pow(2.0, 80))
Old: 1208925819614629200000000
New: 1208925819614629174706176The old parsing algorithms would consider only up to 17 significant digits from the input string and would discard the rest of the digits. This is sufficient to generate a very close approximation of the value represented by the string, and the result is usually very close to the correctly rounded result. The new implementation considers all present digits and produces the correctly rounded result for all inputs (up to 768 digits in length). In addition, these functions now respect the rounding mode (controllable via
fesetround
).
Okay, so it affects the printf
family of functions. Now imagine code that you want to round-trip between a text format and the binary presentation of the number.
Dang!
Well, as luck would have it, I knew a little story. msvcrt.dll
, which originally used to be the C/C++ runtime library of Visual Studio 6, became a system DLL with one of the XP releases 2. The point being that it is an officially anointed part of Windows, rather than a boring old Visual C/C++ runtime ever since. To this very day it is part of Windows and getting updates. And its version resource shows distinctly that it is part of the operating system, rather than Visual C++. It also happens to be a known DLL, which also carries some special semantics 3.
So for msvcrt.dll
we can rely on it being available without having to install some C/C++ runtime redistributable package. Great! What’s more, the obsession Microsoft has for compatibility ensures that we get the behavior Visual C++ 6 programs got. Fortunately that fits exactly the pre-VS2015 era we are looking for. Oh joy. And there’s one more thing: several Windows Driver Kits were standalone toolchains. Originally they depended on Visual Studio and now do again since around VS2015 4. Either way, those standalone WDKs always used the msvcrt.dll
as their C/C++ runtime. They carried the appropriate headers and import libraries. Neat.
Alas, what if we wanted the new behavior and the old behavior? Say in order to have compatibility with the old behavior, but using the new one, going forward?! Is there a way to tickle that out?
There is. And msvcrt.dll
is the key to it. Here’s a little piece of code to demonstrate both the issue (as per the quoted announcement) and the workaround:
#define WIN32_LEAN_AND_MEAN #include#include #include // pow() #include // Prototype for printf (borrowed from stdio.h in WDK 7600.16385.1) typedef int(__cdecl* printf_t)(const char*, ...); #if !defined(_MSVC_LANG) #error This demo is meant for VS2015 and newer #endif int main() { HMODULE hMsvcrt = ::LoadLibrary(_T("msvcrt.dll")); auto const bignumber = pow(2.0, 80); if (hMsvcrt) { auto msvcrt_printf = (printf_t)::GetProcAddress(hMsvcrt, "printf"); if (msvcrt_printf) { msvcrt_printf("Old C/C++ runtime: %.0f [msvcrt.dll]\n", bignumber); } // skipping error handling for brevity } // skipping error handling for brevity printf("New C/C++ runtime: %.0f\n", bignumber); }
The output on Visual Studio 2015 and newer should look something like this:
Old C/C++ runtime: 1208925819614629200000000 [msvcrt.dll] New C/C++ runtime: 1208925819614629174706176
Please note that we’re using the exact same value, simply formatted by different implementations of printf
. That was the main point.
What we have shown with this exercise is that we can indeed get the old behavior from and old library on whose presence we can even rely and at the same time we can make use of the more modern C/C++ runtime, because that’s more standards compliant than the older versions.
// Oliver
PS: please be aware that msvcrt.dll
has indeed evolved beyond what it used to be. So it isn’t the Visual C/C++ 6 runtime library anymore! In fact the WDK from which I borrowed the function prototype 5 carried special object files in the lib
folders used to target the various supported Windows versions at the time: msvcrt_win2000.obj
, msvcrt_winxp.obj
, msvcrt_win2003.obj
. These — to sum it up — carry the glue code between the Windows version shown in the file name, the version named by the containing lib
folder, and the functionality a C/C++ program could expect when using that WDK version. This makes use of the rule that symbols from .obj
files take precedence over symbols from libraries.
- Since I like the writing style, let me recommend this article and this article by Bruce Dawson; you can find other awesome stuff on his blog, including references to useful tools and explanations of difficult to track down defects he has dealt with … [↩]
- I think in one of the service packs [↩]
- … but that’s for another blog post, if at all … [↩]
- … perhaps earlier [↩]
- … which unsurprisingly remained the same as can be found in the modern VS versions … [↩]