Beefy Boxes and Bandwidth Generously Provided by pair Networks
Do you know where your variables are?
 
PerlMonks  

[Win32, C, and way OT] C floats, doubles, and their equivalence

by syphilis (Canon)
on Jul 18, 2009 at 15:43 UTC ( #781347=perlquestion: print w/ replies, xml ) Need Help??
syphilis has asked for the wisdom of the Perl Monks concerning the following question:

Hi,
It doesn't get much more OT than this.
The demo C program:
#include <stdio.h> int main(void) { double nv = 2.0 / 3; float foo = 2.0 / 3; /* Need to do stuff here that ensures that what follows below outputs "False True" */ if(foo == nv) printf("True "); else printf("False "); if(foo == (float)nv) printf("True\n"); else printf("False\n"); return 0; }
Afaict, with any sane C compiler (including MSVC++ 8.0, MSVC++ 9.0, MinGW port of gcc-3.4.5, and gcc-3.2.2 on linux) that program will output "False True".
But with versions of MSVC++ prior to version 8.0, it outputs "False False".

I'm wondering if anyone knows of a workaround that will enable the correct output of "False True" with those earlier versions of MSVC++.

I posted this to comp.os.ms-windows.programmer.misc, and then realized that most posts there now go unanswered and that mine was the first post there in over 5 weeks ... I'm not at all hopeful of a useful response on that forum. Alternatively, if someone knows of a site where this subject is *not* OT, and where there's a good chance of an informed response, then that might also be helpful to me.

Cheers,
Rob

PS For anyone interested, it's not hard to create a script that is essentially the same yet produces the reverse output of "True False". (Again, this applies only to MSVC++ prior to version 8.0.) :
#include <stdio.h> int main(void) { double nv = 2.0 / 3; float foo = nv; /*The only change*/ if(foo == nv) printf("True "); else printf("False "); if(foo == (float)nv) printf("True\n"); else printf("False\n"); return 0; }
But that doesn't help me to solve my problem at all.
My problem is that this bug (as presented in the first script) is stuffing up one particular aspect of PDL when PDL is built using MSVC++ versions that pre-date version 8.0 - and that's what I'm trying to fix.

Comment on [Win32, C, and way OT] C floats, doubles, and their equivalence
Select or Download Code
Re: [Win32, C, and way OT] C floats, doubles, and their equivalence
by ig (Vicar) on Jul 18, 2009 at 15:53 UTC

    Not a solution, but What Every Computer Scientist Should Know About Floating Point Arithmetic may help you understand what is happening and how to code to avoid the problems.

    update: there are many surprising/disappointing features in floating point hardware and software. If you dump the internal representations of the values in your program, you might discern what is happening. I would consider whether values are rounded or truncated when converted from double to float, for example. It appears to be handled differently in the different contexts in your program. If you know what the conversion is doing, you should be able to test and make appropriate adjustments so that the result of the conversion is what you want. Alternatively, you might use a well defined/implemented function to perform the conversion rather than relying on the compiler built-in function.

      I don't think it helps ... unless I've missed something in there.
      Basically, if I have:
      float x = 123.1; float y = 123.1;
      then I expect x == y to be true - and every compiler that I have at my disposal supports that expectation.
      Similarly, if I have:
      float x = 123.1; double y = 123.1;
      then I expect x == (float)y to be true - and every compiler that I have at my disposal (except MSVC++ 7.0) supports that expectation.
      What I'm seeking is some means of bringing the insane MSVC++ 7.0 (and presumably all other MSVC++ versions prior to 8.0) behaviour into line with the behaviour of these other sane compilers.

      It's a question of consistency between compilers. Unless I'm relying on behaviour that the standards classify as "implementation dependent" or "undefined", then it should be the same with all compilers. (If I *am* relying on "implementation dependent"/"undefined" behaviour, then PDL needs a siginificant rewrite - because there are a number of places where the PDL sources assume that behaviour is neither "implementation dependent" nor "undefined".)

      Cheers,
      Rob

        The IEEE standards specify the binary formats, but not the functions (hardware or software) that manipulate them.

        The C standards leave some relevant details undefined or implementation dependent. I don't recall that the exact format of floating point numbers is specified or that the details of conversions are specified (e.g. rounding Vs. truncating).

        There are many relevant hardware and software features that sane people might be inclined to call bugs.

        The operations you are performing in your test program are conceptually simple, yet your experience demonstrates the variability that exists in implementations. For floating point calculations generally, not only does changing the compiler make a difference, but sometimes running the same executable on different hardware makes a difference.

        I would be surprised if Microsoft would fix the old compiler, even if you sent a bug report - their response would almost certainly be that they have fixed it in release 8.0. Even if they did provide a patch for the older compiler, it might be difficult to get everyone compiling your program to install the patch before compiling. You could test for the bug in your test suite and fail the build if it is present.

        If you haven't already, you might try different combinations of of compiler options. Sometimes changing optimization or debug settings will make a difference to such issues.

        If PDL is doing non-trival floating point calculations and must work with a variety of combinations of compiler, libraries and hardware, then it will have to cope with varying results. The paper I linked to gives some good guidance for writing floating point calculations and in particular comparisons to minimize and accommodate the variations.

        Similarly, if I have: float x = 123.1; double y = 123.1; then I expect x == (float)y to be true

        I don't know that that is a reasonable expectation. Conversion from double to float is not the exact same operation as the initial assignment. The first is converting the numeric string to a floating point representation. The resulting floating point representation will be very very close to but not necessarily exactly "123.1" or whatever other value you assign to it. By contrast converting double to float is reducing the number of significant digits in a representation that is already approximate. Mismatches between assignment and rounding are always possible.

        In general, exact equalities on doubles and floats is not a good idea. I'm a little surprised that PDL code is assuming that it is. I was always taught instead to do x - y < EPSILON where EPSILON is some value small enough to make the difference between the two numbers irrelevant for the purposes of your computation. If exact decimal precision is needed (as is sometimes the case in financial calculations) then one needs to use one of the many C/C++ arbitrary precision libraries.

        Best, beth

        Update: x - y < EPSILON assumes it is known that x >= y (i.e. prior code has tested that assertion). In the general case where that assertion has not been verified, see comment by roboticus below.

Re: [Win32, C, and way OT] C floats, doubles, and their equivalence
by creamygoodness (Curate) on Jul 18, 2009 at 16:32 UTC

    Seems like the conversion glitch could be happening at several places.

    Maybe the initial assignment of float foo needs a cast. MSVC warns about loss of precision in such cases -- maybe it's serious. :)

    float foo = (float)nv;

    As a workaround, perhaps you could truncate the initial assignment of the double:

    double nv = (float)(2.0 / 3);

    Lastly, maybe something is awry in the conversions that are being done during the comparison -- like it's ignoring your cast. It's a stab in the dark, but maybe a hack like this would get it to pay attention:

    static __inline int compare_truncated_double_to_float(double d, float f) { float truncted_double = (float)d; if (f == truncated_double) { return 1; } else { return 0; } }

    PS: This is relevant to the work I do ensuring Windows compatibility for my XS distros.

    --
    Marvin Humphrey
      It's a stab in the dark ...

      Good stab - I can make use of that. The following outputs "True False" on the buggy compilers (and "True True" on sane compilers):
      #include <stdio.h> int main(void) { double nv = 2.0 / 3; float foo = 2.0 / 3; float truncated = (float)nv; if(foo == truncated) printf("True "); else printf("False "); if(foo == (float)nv) printf("True\n"); else printf("False\n"); return 0; }
      I can apply that method (used to obtain that "True") to PDL, which solves the problem I have asked about. Unfortunately, while it enables me to get the behaviour I want with my C demo scripts, it's still not producing the correct result with PDL - but this is such a fickle bug. I'll have to play around with it some more.

      PS: This is relevant to the work I do ensuring Windows compatibility for my XS distros

      #if defined _MSC_VER && _MSC_VER < 1400 then expect weirdness if you start comparing floats with doubles.

      Thanks Marvin.

      Cheers,
      Rob
Re: [Win32, C, and way OT] C floats, doubles, and their equivalence
by ig (Vicar) on Jul 18, 2009 at 17:33 UTC

    The most direct route to identifying where the "error" arises would be to run your test program in the debugger, stepping through the machine code and checking the internal representations of the values at relevant points. It should be straight forward to identify which operation is in error and what the nature of the error is.

    Having determined where the error is, you can write a correct function to replace the faulty one. Of course, adding explicit function calls to replace every typecast and implicit conversion will be non-trivial.

Re: [Win32, C, and way OT] C floats, doubles, and their equivalence
by BrowserUk (Pope) on Jul 18, 2009 at 18:45 UTC

    The problem is routed in how v6, (pre-v8 maybe, but I only have v6 and v8), generates the code. The following 'fixes' the problem, though I realise that it may not be a workable solution for you:

    #include <stdio.h> cmpFsFd( float s, double d ) { float tmp = (float)d; return s == tmp ? 1 : 0; } int main(void) { double nv = 2.0 / 3; float foo = 2.0 / 3; if( foo == nv ) printf("True "); else printf("False "); if( cmpFsFd( foo, nv )) printf("True\n"); else printf("False\n"); return 0; }
    [19:48:23.40} C:\test>cl float.c /Fefloatv8.exe Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 15.00.30729.01 +for 80x86 Copyright (C) Microsoft Corporation. All rights reserved. float.c Microsoft (R) Incremental Linker Version 9.00.30729.01 Copyright (C) Microsoft Corporation. All rights reserved. /out:floatv8.exe float.obj [19:49:59.60} C:\test>floatv8 False True ---------------------------------------------------- c:\test>cl float.c /Fefloatv6.exe Microsoft (R) 32-bit C/C++ Standard Compiler Version 13.00.9466 for 80 +x86 Copyright (C) Microsoft Corporation 1984-2001. All rights reserved. float.c Microsoft (R) Incremental Linker Version 7.00.9466 Copyright (C) Microsoft Corporation. All rights reserved. /out:floatv6.exe float.obj c:\test>floatv6 False True

    Basically, when you coerce a double to a float before comparing it to a float, you need to force the compiler to store the coerced value as a float before doing the comparison. That's what my cmpFsFd() is doing. (Insert underscores to taste :)

    The reasoning is that it is only when the values are stored to memory (moved out of the FP registers), that the actual rounding/truncation occurs. Whilst values remain within the FP registers they are maintained as 80-bit FP values, regardless of whether they originate as 32-bit or 64-bit FPs.

    The v8 (and presumably other compilers) do the coercion ((float)nv), by storing and and reloading to a temporary 32-bit memory location:

    ; 15 : if( foo == (float)nv ) printf("True\n"); fld QWORD PTR _nv$[ebp] ## Load nv onto FPU stack fstp DWORD PTR tv79[ebp] ## store (and pop) it into a 32-bit ( +float) temporary fld DWORD PTR tv79[ebp] ## load it back onto the FPU stack fld DWORD PTR _foo$[ebp] ## load foo onto the FPU stack fucompp ## do the comparison fnstsw ax ## get the FPU status word into AX test ah, 68 ## 00000044H (Check for equality?) jp SHORT $LN2@main ## Jump push OFFSET $SG2485 ## or not ... call _printf

    The equivalent code generated by the V6 compiler omits that store & load step:

    ; 15 : if( foo == (float)nv ) printf("True\n"); fld QWORD PTR _nv$[ebp] ## Load nv to FPU stack fst DWORD PTR tv78[ebp] ## Store it to a temporary but... *** NEVER LOADS IT BACK *** *** And does the comparison between the FPU register and the m +emory image of foo *** fcomp DWORD PTR _foo$[ebp] fnstsw ax test ah, 68 ; 00000044H jp SHORT $L800 push OFFSET FLAT:$SG801 call _printf

    On the v7 compiler, you might get away with using /fp:strict or /fp:precise, but the v6 compiler lacks these options. (For the same reason, I haven't been able to check that theory!)

    Maybe someone can come up with a preprocessor macro to map (float)x to something like ( float tmp = (float)d )? (Some of the macros in the Perl sources seem to do equally obscure things, but they fairly make my skin crawl :)

    Personally, I'd prefer using cmpFsFd(), and perhaps an editor macro (with manual yea/nay) to change the sources. If the function was marked inline, it might not impose to much of a performance penalty, but you might have to be careful that the compiler doesn't optimise the tmp var away.

    Anyway, I hope that is of some use to you.

    Reference: http://webster.cs.ucr.edu/AoA/Windows/HTML/RealArithmetica2.html

    11.2.5 Conversions The FPU performs all arithmetic operations on 80 bit real quantities. In a sense, the FLD and FST/FSTP instructions are conversion instructions as well as data movement instructions because they automatically convert between the internal 80 bit real format and the 32 and 64 bit memory formats.

    Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
    "Science is about questioning the status quo. Questioning authority".
    In the absence of evidence, opinion is indistinguishable from prejudice.
      I think your detailed investigation demonstrates why the code I posted in repsonse to creamygoodness's post behaves the way it does.

      After much poking and scratching and re-reading of suggestions that have been kindly and thoughtfully presented here, I've eventually come up with using this approach in the PDL code:
      #include <stdio.h> #if defined _MSC_VER && _MSC_VER < 1400 #pragma optimize("", off) #endif int main(void) { #if defined _MSC_VER && _MSC_VER < 1400 double nv = 2.0 / 3; float foo = 2.0 / 3; float dummy = (float)nv; if(foo == dummy) printf("True "); else printf("False "); #else double nv = 2.0 / 3; float foo = 2.0 / 3; if(foo == (float)nv) printf("True "); else printf("False "); #endif return 0; }
      It seems to be doing the right thing with all of my compilers.
      I don't think that C script needs to have the optimization turned off - the creation of the dummy variable is alone sufficient to get the behaviour I'm after. But for some reason, in the PDL code, creation of the dummy variable is *not*, by itself, sufficient - optimization also needs to be disabled. (I turn it off for the setvaltobad functions, then turn it back on again.)

      Thanks to *all* who replied.

      Cheers,
      Rob
        But for some reason, in the PDL code, creation of the dummy variable is *not*, by itself, sufficient - optimization also needs to be disabled. (I turn it off for the setvaltobad functions, then turn it back on again.)

        That's why I moved the temp var and comparison into a separate function; it forces the compiler to use the temp value from memory for the comparison:

        ; 5 : return s == tmp ? 1 : 0; fld DWORD PTR _s$[ebp] fcomp DWORD PTR _tmp$[ebp] fnstsw ax test ah, 68 ; 00000044H jp SHORT $L809

        The problem with the test script is that with optimisations enabled, the newer compiler is able to reduce the whole script to a simple printf( "False" ); printf( "True" ); return 0; (even with the use of the sub) as everything is known at compile time:

        ; Listing generated by Microsoft (R) Optimizing Compiler Version 15.00 +.30729.01 TITLE C:\test\float.c .686P .XMM include listing.inc .model flat INCLUDELIB LIBCMT INCLUDELIB OLDNAMES _DATA SEGMENT $SG2527 DB 'True ', 00H ORG $+2 $SG2529 DB 'False ', 00H ORG $+1 $SG2531 DB 'True', 0aH, 00H ORG $+2 $SG2533 DB 'False', 0aH, 00H _DATA ENDS PUBLIC _cmpFsFd EXTRN __fltused:DWORD ; Function compile flags: /Ogtpy ; File c:\test\float.c ; COMDAT _cmpFsFd _TEXT SEGMENT tv135 = 8 ; size = 4 _s$ = 8 ; size = 4 _d$ = 12 ; size = 8 _cmpFsFd PROC ; COMDAT ; 4 : float tmp = (float)d; ; 5 : return s == tmp ? 1 : 0; fld DWORD PTR _s$[esp-4] fld QWORD PTR _d$[esp-4] fstp DWORD PTR tv135[esp-4] fld DWORD PTR tv135[esp-4] fucompp fnstsw ax test ah, 68 ; 00000044H jp SHORT $LN3@cmpFsFd mov eax, 1 ; 6 : } ret 0 $LN3@cmpFsFd: ; 4 : float tmp = (float)d; ; 5 : return s == tmp ? 1 : 0; xor eax, eax ; 6 : } ret 0 _cmpFsFd ENDP _TEXT ENDS PUBLIC _main EXTRN _printf:PROC ; Function compile flags: /Ogtpy _TEXT SEGMENT _main PROC ; 11 : double nv = 2.0 / 3; ## ; 12 : float foo = 2.0 / 3; ## All this and ... ; 13 : ; 14 : if( foo == nv ) printf("True "); ## ; 15 : else printf("False "); push OFFSET $SG2529 call _printf ; 16 : ; 17 : if( cmpFsFd( foo, nv ) ) printf("True\n"); ## this are op +timised away! push OFFSET $SG2531 call _printf add esp, 8 ; 18 : else printf("False\n"); ; 19 : ; 20 : return 0; xor eax, eax ; 21 : } ret 0 _main ENDP _TEXT ENDS END

        Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
        "Science is about questioning the status quo. Questioning authority".
        In the absence of evidence, opinion is indistinguishable from prejudice.

      but you might have to be careful that the compiler doesn't optimise the tmp var away.

      Use volatile to force the compiler to avoid optimising the memory lookup away.

        Use volatile to force the compiler to avoid optimising the memory lookup away

        I think the above suggestion is made in relation to the use of a separate function - which is not the method I've adopted. (I've made use of a temp variable, but it's in the body of the function itself, rather than in a separate function.)

        I find that declaring my temp variable as "volatile" doesn't help me. If I get rid of the #pragma optimize() calls, and declare the temp variable as volatile float temp, the problem remains. Obviously, "volatile" doesn't turn off every kind of optimization, and certainly doesn't turn off the kind of optimization that it needs to (in my case).

        Cheers,
        Rob

Log In?
Username:
Password:

What's my password?
Create A New User
Node Status?
node history
Node Type: perlquestion [id://781347]
Approved by Bloodnok
help
Chatterbox?
and the web crawler heard nothing...

How do I use this? | Other CB clients
Other Users?
Others surveying the Monastery: (18)
As of 2014-10-24 14:56 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    For retirement, I am banking on:










    Results (132 votes), past polls