NO

Author Topic: Different result with -O2 than without it  (Read 556 times)

Offline FRex

  • Member
  • *
  • Posts: 4
Different result with -O2 than without it
« on: March 26, 2019, 04:54:05 am »
Code: (c) [Select]
#include <stdio.h>

#define cast(x) ((unsigned char)((x)&255))

int main(int argc, char ** argv)
{
    unsigned char b = 255;
    unsigned char x =  cast((b + b) >> 1);
    unsigned y      = cast((b + b) >> 1);
    printf("0x%02x 0x%02x\n", x, y);
    return 0;
}

This program when compiled with -O2 will print "0x7f 0xff" despite x and y being assigned the same expression and 0xff fitting in both of their types.

Without -O2 and on other compilers it prints "0xff 0xff" (which IMO is correct due to integral promotions taking place).

It's as if with -O2 the compiler stores intermediate result in target variable between operations (easy to confirm by setting b to anything from 128 to 255).

I ran into this problem (not as simple as this example) in png decoding code of https://github.com/nothings/stb/blob/master/stb_image.h

Offline TimoVJL

  • Global Moderator
  • Member
  • *****
  • Posts: 1857
Re: Different result with -O2 than without it
« Reply #1 on: March 26, 2019, 09:56:06 am »
A bit tricky:
Quote
14 Environment §5.1.2.3
10
EXAMPLE 2 In executing the fragment
char c1, c2;
/* ... */
c1 = c1 + c2;
the ‘‘integer promotions’’ require that the abstract machine promote the value of each variable to int size
and then add the two ints and truncate the sum. Provided the addition of two chars can be done without
overflow, or with overflow wrapping silently to produce the correct result, the actual execution need only
produce the same result, possibly omitting the promotions.

Code: [Select]
#include <stdio.h>

#define cast(x) ((unsigned char)((x)&255))

int main(int argc, char ** argv)
{
    unsigned char b = 255;
    unsigned char x = cast((b + b) >> 1);
    unsigned y      = cast((b + b) >> 1);
    unsigned char z = (b + b) >> 1;
    printf("0x%02x 0x%02x 0x%02x\n", x, y, z);
    printf("b+b = 0x%02x, (b + b) >> 1 = 0x%02x\n", b + b, (b + b) >> 1 );
    return 0;
}
« Last Edit: March 26, 2019, 04:27:17 pm by TimoVJL »
May the source be with you

Offline FRex

  • Member
  • *
  • Posts: 4
Re: Different result with -O2 than without it
« Reply #2 on: March 26, 2019, 03:07:51 pm »
I don't see the trickiness, this is a compiler mistake when optimizing IMO, because the result is definitely not correct, especially the part where value of an uchar expression is 255 (or any other where highest bit is set) if it's assigned to a bigger type and 127 (highest bit unset because it was 9th bit of the intermediary sum and got discarded before the shift) if it's assigned to an uchar (but only in -O2 !).

An expression's value shouldn't change depending on the type it's assigned to (and only in -O2), both uchar and bigger types can fit 255 that this expression produces and it's type (via the cast) is even uchar already.

It also doesn't happen with MSVC, GCC and Clang and stb_image is sort of popular so if it was routinely corrupting some pngs (that happen to use the average filter which has this '(byte + otherbyte) >> 1' expression in decoder) it'd be noticed and fixed by now.

Edit: the bug here happens no matter if values are known at compile time and precomputed or not, e.g. it happens while doing 'unsigned char b = (atoi(argv[1]) & 255);' and passing 255 arg too. I'm just adding that since I was looking into it more and noticed asm output with -O2 of the original example has a precomputed 255 and 127 in it but it happens with values only known at runtime too (I first ran into it while loading certain png files).

Edit 2: a similar bug is triggered with two unsigned shorts that do '(a + b) >> 1' assigned to an unsigned short. If you look at optimized ASM output it's as if when the type of the destination variable to which '(a + b) >> 1' is assigned is same as type of a and b, then the intermediate result of the add is stored in that register too, which for high values of a and b (above 127 for uchar, above 32767 for ushort) discards the top bit that's a 1 (and that would be kept if integer promotion to int was done), so then shifting right once shifts a zero into top spot instead of the 1 that should be there, causing this bug.
« Last Edit: March 27, 2019, 10:39:01 pm by FRex »

Offline stecoop

  • Member
  • *
  • Posts: 5
Re: Different result with -O2 than without it
« Reply #3 on: April 26, 2019, 03:54:21 am »
I've run into a similar problem; If I have the -O2 (or -Ot as it shows in the compiler options settings), two of my programs display incorrect results when scaling the display for graphing, or rotating text for display. If I compile with no optimizations, both programs work correctly.

Is there something I can change so that all new projects default to No Optimizations?

Also, I've noticed when using the debugger, and stopped at a breakpoint, the debugger shows all ints as BOOLs (thats BOOL, not _Bool), not as ints.

Comments, anybody?
I was born the same month and year FORTRAN was "born".