Bug 85957 - i686: Integers appear to be different, but compare as equal
Summary: i686: Integers appear to be different, but compare as equal
Status: REOPENED
Alias: None
Product: gcc
Classification: Unclassified
Component: c (show other bugs)
Version: 7.3.1
: P3 normal
Target Milestone: ---
Assignee: Not yet assigned to anyone
URL:
Keywords:
Depends on:
Blocks:
 
Reported: 2018-05-28 19:05 UTC by Luke Shumaker
Modified: 2018-05-31 22:50 UTC (History)
2 users (show)

See Also:
Host:
Target:
Build:
Known to work:
Known to fail:
Last reconfirmed: 2018-05-29 00:00:00


Attachments
The preprocessed source (53.91 KB, text/plain)
2018-05-28 19:05 UTC, Luke Shumaker
Details

Note You need to log in before you can comment on or make changes to this bug.
Description Luke Shumaker 2018-05-28 19:05:54 UTC
Created attachment 44200 [details]
The preprocessed source

This is a bug that at first looks a bit like a "problems with floating
point numbers" bug.  However, my problem is with integers (calculated
from float types) behave inconsistently.

    a6 = a.dbl * 1e6;
    b6 = b.dbl * 1e6;
    printf ("a6 = %llu\n", a6); // prints "1"
    printf ("b6 = %llu\n", b6); // prints "0"
    printf ("(a6 == b6) = %s\n", (a6 == b6) ? "true" : "false"); // prints "true"

I understand why floating point math could result in a6 and b6 being
different; my concern is that a6 and b6 (which are integer types)
appear to be different, yet compare as being equal.

This happens on i686 with -O1 and -O2 (but not -O0), and not on
x86-64.

I apologize that my minimal testcase makes use of the glib-2.0
library; I'm having a hard time replicating the problem without it; it
seems GCC optimizing out a variable is key; and removing the library
use makes it not optimize it out.

Here is the output of gcc, including the appropriate version information:

    $ gcc -v -save-temps -O1 $(pkg-config --libs --cflags glib-2.0) demo.c -o demo

    Using built-in specs.
    COLLECT_GCC=/usr/bin/gcc
    COLLECT_LTO_WRAPPER=/usr/lib/gcc/i686-pc-linux-gnu/7.3.1/lto-wrapper
    Target: i686-pc-linux-gnu
    Configured with: /build/gcc/src/gcc/configure --prefix=/usr --libdir=/usr/lib --libexecdir=/usr/lib --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=https://bugs.archlinux.org/ --enable-languages=c,c++,ada,fortran,go,lto,objc,obj-c++ --enable-shared --enable-threads=posix --enable-libmpx --with-system-zlib --with-isl --enable-__cxa_atexit --disable-libunwind-exceptions --enable-clocale=gnu --disable-libstdcxx-pch --disable-libssp --enable-gnu-unique-object --enable-linker-build-id --enable-lto --enable-plugin --enable-install-libiberty --with-linker-hash-style=gnu --enable-gnu-indirect-function --disable-multilib --disable-werror --enable-checking=release --enable-default-pie --enable-default-ssp
    Thread model: posix
    gcc version 7.3.1 20180312 (GCC) 
    COLLECT_GCC_OPTIONS='-v' '-save-temps' '-O1' '-I' '/usr/include/glib-2.0' '-I' '/usr/lib/glib-2.0/include' '-o' 'demo' '-mtune=generic' '-march=pentiumpro'
     /usr/lib/gcc/i686-pc-linux-gnu/7.3.1/cc1 -E -quiet -v -I /usr/include/glib-2.0 -I /usr/lib/glib-2.0/include demo.c -mtune=generic -march=pentiumpro -O1 -fpch-preprocess -o demo.i
    ignoring nonexistent directory "/usr/lib/gcc/i686-pc-linux-gnu/7.3.1/../../../../i686-pc-linux-gnu/include"
    #include "..." search starts here:
    #include <...> search starts here:
     /usr/include/glib-2.0
     /usr/lib/glib-2.0/include
     /usr/lib/gcc/i686-pc-linux-gnu/7.3.1/include
     /usr/local/include
     /usr/lib/gcc/i686-pc-linux-gnu/7.3.1/include-fixed
     /usr/include
    End of search list.
    COLLECT_GCC_OPTIONS='-v' '-save-temps' '-O1' '-I' '/usr/include/glib-2.0' '-I' '/usr/lib/glib-2.0/include' '-o' 'demo' '-mtune=generic' '-march=pentiumpro'
     /usr/lib/gcc/i686-pc-linux-gnu/7.3.1/cc1 -fpreprocessed demo.i -quiet -dumpbase demo.c -mtune=generic -march=pentiumpro -auxbase demo -O1 -version -o demo.s
    GNU C11 (GCC) version 7.3.1 20180312 (i686-pc-linux-gnu)
    	compiled by GNU C version 7.3.1 20180312, GMP version 6.1.2, MPFR version 4.0.1, MPC version 1.1.0, isl version isl-0.18-GMP
    
    GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
    GNU C11 (GCC) version 7.3.1 20180312 (i686-pc-linux-gnu)
    	compiled by GNU C version 7.3.1 20180312, GMP version 6.1.2, MPFR version 4.0.1, MPC version 1.1.0, isl version isl-0.18-GMP
    
    GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
    Compiler executable checksum: b94f7ca39249d495c6913c6ded8c0b64
    COLLECT_GCC_OPTIONS='-v' '-save-temps' '-O1' '-I' '/usr/include/glib-2.0' '-I' '/usr/lib/glib-2.0/include' '-o' 'demo' '-mtune=generic' '-march=pentiumpro'
     as -v -I /usr/include/glib-2.0 -I /usr/lib/glib-2.0/include --32 -o demo.o demo.s
    GNU assembler version 2.30 (i686-pc-linux-gnu) using BFD version (GNU Binutils) 2.30
    COMPILER_PATH=/usr/lib/gcc/i686-pc-linux-gnu/7.3.1/:/usr/lib/gcc/i686-pc-linux-gnu/7.3.1/:/usr/lib/gcc/i686-pc-linux-gnu/:/usr/lib/gcc/i686-pc-linux-gnu/7.3.1/:/usr/lib/gcc/i686-pc-linux-gnu/
    LIBRARY_PATH=/usr/lib/gcc/i686-pc-linux-gnu/7.3.1/:/usr/lib/gcc/i686-pc-linux-gnu/7.3.1/../../../:/lib/:/usr/lib/
    COLLECT_GCC_OPTIONS='-v' '-save-temps' '-O1' '-I' '/usr/include/glib-2.0' '-I' '/usr/lib/glib-2.0/include' '-o' 'demo' '-mtune=generic' '-march=pentiumpro'
     /usr/lib/gcc/i686-pc-linux-gnu/7.3.1/collect2 -plugin /usr/lib/gcc/i686-pc-linux-gnu/7.3.1/liblto_plugin.so -plugin-opt=/usr/lib/gcc/i686-pc-linux-gnu/7.3.1/lto-wrapper -plugin-opt=-fresolution=demo.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s --build-id --eh-frame-hdr --hash-style=gnu -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -pie -o demo /usr/lib/gcc/i686-pc-linux-gnu/7.3.1/../../../Scrt1.o /usr/lib/gcc/i686-pc-linux-gnu/7.3.1/../../../crti.o /usr/lib/gcc/i686-pc-linux-gnu/7.3.1/crtbeginS.o -L/usr/lib/gcc/i686-pc-linux-gnu/7.3.1 -L/usr/lib/gcc/i686-pc-linux-gnu/7.3.1/../../.. -lglib-2.0 demo.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc/i686-pc-linux-gnu/7.3.1/crtendS.o /usr/lib/gcc/i686-pc-linux-gnu/7.3.1/../../../crtn.o
    COLLECT_GCC_OPTIONS='-v' '-save-temps' '-O1' '-I' '/usr/include/glib-2.0' '-I' '/usr/lib/glib-2.0/include' '-o' 'demo' '-mtune=generic' '-march=pentiumpro'

Attached is the preprocessed demo.i file.
Comment 1 Andrew Pinski 2018-05-28 19:12:42 UTC
>(calculated from float types)

Which is exactly just that.
64bit float point does not have 64bit of precision but rather 53bits.
On x86, since it uses 80bit fpu internally and does not round between the intermediate steps which is why you are getting two different answers.

*** This bug has been marked as a duplicate of bug 323 ***
Comment 2 Luke Shumaker 2018-05-28 20:39:22 UTC
I do not believe that this is a duplicate of bug 323.  As I wrote:

> As I understand why floating point math could result in a6 and b6 being
> different; my concern is that a6 and b6 (which are integer types)
> appear to be different, yet compare as being equal.

"a6" and "b6" are both variables with types that resolve to "long long unsigned integer".

    printf ("a6 = %llu\n", a6); // prints "a6 = 1"
    printf ("b6 = %llu\n", b6); // prints "b6 = 0"

That's fine, I understand that a6 and b6 could be different because of differing round-off between intermediate steps.  That's not my concern.

Note that a6 and b6 have should have concrete values at this point, as we have printed them.

My concern is the following:

    printf ("(a6 == b6) = %s\n",
            (a6 == b6) ? "true" : "false"); // prints "(a6 == b6) = true"

That is, the entire output of the POC program is:

    a6 = 1
    b6 = 0
    a6 == b6

I am not concerned that a6 and b6 disagree, or that they are equal.  I am concerned that *both* are true.
Comment 3 Andrew Pinski 2018-05-28 21:17:38 UTC
There is still rounding errors when it comes to the math you are doing.

*** This bug has been marked as a duplicate of bug 323 ***
Comment 4 Vincent Lefèvre 2018-05-28 22:32:30 UTC
(In reply to Andrew Pinski from comment #3)
> There is still rounding errors when it comes to the math you are doing.

Yes, but the issue here is much more serious, and I don't see this bug as a duplicate (bug 323 is just a cause of this more serious bug).

While it has been accepted that a floating-point variable can be multi-valued (except in C99/C11 modes), this must not be the case on a variable of integer type, even though the value of such a variable has been computed from a floating-point expression: Once a floating-point number has been converted into an integer type, the value of this integer must be fixed.
Comment 5 Andrew Pinski 2018-05-28 22:36:32 UTC
Try -std=c99 or -fexcess-precision=standard which will get you the behavior you want.
Comment 6 Vincent Lefèvre 2018-05-28 23:28:12 UTC
(In reply to Andrew Pinski from comment #5)
> Try -std=c99 or -fexcess-precision=standard which will get you the behavior
> you want.

This is not what is documented: "By default, -fexcess-precision=fast is in effect; this means that operations may be carried out in a wider precision than the types specified in the source if that would result in faster code, and it is unpredictable when rounding to the types specified in the source code takes place."

This means that in

  double x = 1.1 * 1.2;

x can be kept with excess precision (typically 64 bits instead of 53) or can be rounded to double depending on its use.

But here, one has:

  unsigned long long a6 = a.dbl * 1e6;

This is no longer just a rounding of a floating-point value, but a conversion to an integer type. From -fexcess-precision=fast, one cannot decide whether a6 will be 0 or 1, but once the value of a6 has been observed, it should no longer be allowed to change.
Comment 7 Alexander Monakov 2018-05-29 17:28:53 UTC
Reopening, the issue here is way more subtle than bug 323 and points to a possible issue in DOM. Hopefully Richi can have a look and comment.

It appears dom2 pass performs something like jump threading based on compile-time-evaluated floating-point expression values without also substituting those expressions in IR. At run time, they are evaluated to different values, leading to an inconsistency. Namely, dom2 creates bb 10:

  <bb 9>:
  # iftmp.1_1 = PHI <"true"(7), "false"(8), "true"(10)>
  printf ("(a6 == b6) = %s\n", iftmp.1_1);
  return 0;

  <bb 10>:
  _24 = __n2_13 * 1.0e+6;
  b6_25 = (guint64) _24;
  printf ("a6 = %llu\n", 1);
  printf ("b6 = %llu\n", b6_25);
  goto <bb 9>;

where jump to bb 9 implies that _24 evaluates to 1.0 and b6_25 to 1, but they are not substituted as such, and at run time evaluate to 0.99... and 0 due to excess precision.

The following reduced testcase demonstrates the same issue, but requires -fdisable-tree-dom3 (on gcc-6 at least, as otherwise dom3 substitutes results of compile-time evaluation).

__attribute__((noinline,noclone))
static double f(void)
{
  return 1e-6;
}

int main(void)
{
  double a = 1e-6, b = f();

  if (a != b) __builtin_printf("uneq");

  unsigned long long ia = a * 1e6, ib = b * 1e6;

  __builtin_printf("%lld %s %lld\n", ia, ia == ib ? "==" : "!=", ib);
}
Comment 8 Alexander Monakov 2018-05-30 04:05:21 UTC
To expand a bit: DOM makes the small testcase behave as if 'b' and 'ib' are evaluated twice:

* one time, 'b' is evaluated in precision matching 'a' (either infinite or double), and 'ib' is evaluated to 1; this instance is used in 'ia == ib' comparison;
* a second time, 'b' is evaluated in extended precision and 'ib' is evaluated to 0; this instance is passed as the last argument to printf.

This is surprising as the original program clearly evaluates 'b' and 'ib' just once.

If there's no bug in DOM and the observed transformation is allowed to happen when -fexcess-precision=fast is in effect, I think it would be nice to mention that in the compiler manual.
Comment 9 Alexander Monakov 2018-05-30 04:15:58 UTC
Sorry, the above comment should have said 'b * 1e6' every time it said 'b'.
Comment 10 Alexander Monakov 2018-05-30 06:06:55 UTC
Also note that both the original and the reduced testcase can be tweaked to exhibit the surprising transformation even when -fexcess-precision=standard is enabled. A "lazy" way is via -mpc64, but I think it's possible even without the additional option (by making the code more convoluted to enforce rounding to double). Here's what happens on the reduced testcase:

$ gcc -m32 d.c -O -fdisable-tree-dom3 && ./a.out 
cc1: note: disable pass tree-dom3 for functions in the range of [0, 4294967295]
1 == 0

$ gcc -m32 d.c -O -fdisable-tree-dom3 -fexcess-precision=standard -mpc64 && ./a.out                                                                                                                                
cc1: note: disable pass tree-dom3 for functions in the range of [0, 4294967295]
0 == 1
Comment 11 joseph@codesourcery.com 2018-05-31 22:50:38 UTC
On Mon, 28 May 2018, vincent-gcc at vinc17 dot net wrote:

> floating-point expression: Once a floating-point number has been converted into
> an integer type, the value of this integer must be fixed.

Yes, I agree that any particular conversion to integer executed in the 
abstract machine must produce some definite integer value for each 
execution.

(Conversions of *out-of-range* floating-point values to integer are a 
trickier case; under Annex F they produce unspecified values.  I think 
semantics along the lines of N2221 are fine for unspecified values arising 
from reading an uninitialized object, but more questionable for values 
arising from a floating-point-to-integer conversion.)