LWN.net Logo

Sure, but then you deserve what you get...

Sure, but then you deserve what you get...

Posted May 24, 2011 10:05 UTC (Tue) by khim (subscriber, #9252)
In reply to: It's different viewpoints... by etienne
Parent article: What Every C Programmer Should Know About Undefined Behavior #3/3

Same for debugging, I may want to display some binary values as unsigned in hexadecimal even if I know that is a float (so a dirty cast), to see if I had a memory overflow and in fact this float contains a string...

Right, but how often you actually do that? Do you really want to punish the compiler and force it to keep "for loop iterative variable" in memory to make sure it's changed via float if the pointers are mixed just right?

My favorite example is bug 33498. It's this piece of code:

void table_init(int *value)
{
        int i;
        int val = 0x03020100;

        for (i = 0; i < 256/4; i++) {
                value[i] = val;
                val += 0x04040404;
        }
}

What does this piece of code do? Most people answer: it fills the table. Smart people answer: it efficiently fills the table of chars using trick with type conversions. At that point I show what it really does: it destroys your program. Oops? Nasty bug in the compiler? Nope: Status: RESOLVED INVALID.

The problem with undefined behavior today is not that compilers peruse it for optimizations. It's the fact that they do it so carefully. That means that a lot of cases which should trigger undefined behavior don't blow up but silently work - this means people become complacent. Hopefully LTO will help: it'll allow the compiler to weed the undefined behavior branches more aggressively and this will mean people will be punished earlier and learn to look for undefined behavior cases.


(Log in to post comments)

Sure, but then you deserve what you get...

Posted May 24, 2011 11:20 UTC (Tue) by farnz (guest, #17727) [Link]

But look at comment 10 to that bug; Eric's not a fool, and yet his mental model of the C89 virtual machine tells him that the following code has implementation-defined semantics, not undefined semantics. In particular, he assumes that i will overflow in a defined fashion, although the final value is not predictable:

int i = INT_MAX;
int j;
int *location = some_sane_value;
for( j = 0; j < 100; ++j )
{
    location[j] = i++;
}

So, the question is why does Eric think that way? I would suggest that one reason is that an assembly language equivalent does have well-defined semantics (using an abstract machine that's a bit like ARM):

MOV R0, #INT_MAX
ADR R1, some_sane_value;
MOV R2, 0
.loop:
MOV [R1 + R2 * 4], R0
ADD R0, R0, #1
ADD R2, R2, #1
CMP R2, #100
BLT .loop

In this version of the code, which is roughly what an intuition of "C is a high level assembly language" would compile the source to, ADD R0, R0, #1 has defined overflow semantics; further, exiting the loop depends on the final value of R2, not on the value of R0. The surprise for Eric is twofold:

  1. The compiler has chosen to elide j, and exit the loop when i reaches its final value.
  2. Because i is signed, i's final value is undefined, thus the compiler never exits the loop.

If i was unsigned, and we changed INT_MAX to UINT_MAX, Eric would probably still have been surprised that his loop compiled to something like:

MOV R0, #UINT_MAX
ADR R1, some_sane_value
.loop:
MOV [R1], R0
ADD R0, R0, #1
ADD R1, R1, #4
CMP R0, #UINT_MAX + 100
BNE .loop

Assuming I'm right in thinking that it's the mental model caused by "C is a high level assembly language" that's breaking things, we have an open question: how do we change the way developers think about C such that perfectly correct compilers don't surprise them?

Sure, but then you deserve what you get...

Posted May 24, 2011 15:16 UTC (Tue) by foom (subscriber, #14868) [Link]

I think it's pretty ridiculous that signed overflow is undefined but unsigned overflow is defined. That's an low-level accident of history (some architectures used 2's complement for signed values, some used 1's complement). That this mistake now gets used as a loophole to make programs be optimized better (or optimized into running incorrectly) is a nice trick, but doesn't really seem justifiable if you were going to do it again.

Signed overflow *ought to have been* implementation defined in C from the beginning, not undefined.

Sure, but then you deserve what you get...

Posted May 24, 2011 16:08 UTC (Tue) by marcH (subscriber, #57642) [Link]

> That this mistake now gets used as a loophole to make programs be optimized better (or optimized into running incorrectly) is a nice trick,

I do not think this is a fair description of what gcc does here.

Please correct if I'm wrong: I think what happens here is just that gcc has a small and *unexpected* "overflow accident" while optimizing the end condition of the loop. And that is OK because signed overflows are simply not supported in C.

Of course it would be nice to have a warning; I guess the main article already explained why this is difficult.

Sure, but then you deserve what you get...

Posted May 24, 2011 16:48 UTC (Tue) by farnz (guest, #17727) [Link]

Reading the bug report, GCC specifically takes advantage of the undefinedness of signed overflow. We start with the following code (all code in a C-like pseudocode):

int *value;
int i;
int val = 0x03020100;

for (i = 0; i < 256/4; i++) {
    value[i] = val;
    val += 0x04040404;
}

Step 1 of the failure determines that the only time val equals 0x04030200 is when the loop exit condition is true. It thus rewrites the program to look as if the programmer had written:

int *value;
int i;
int val = 0x03020100;

for (i = 0; val != 0x04030200; i++) {
    value[i] = val;
    val += 0x04040404;
}

Next, GCC detects that in the absence of overflow, (val != 0x04030200) is always true. It thus rewrites the program to look as if the programmer had written:

int *value;
int i;
int val = 0x03020100;

for (i = 0; true; i++) {
    value[i] = val;
    val += 0x04040404;
}

This code then translates using the naïve interpretation to the assembler output in the bug report. Note that a finite loop has become infinite; this is a useful optimization because it's not uncommon for real world code to have conditionals that depend solely on constants, such that for this build, the conditional is always true or always false.

Sure, but then you deserve what you get...

Posted May 24, 2011 22:18 UTC (Tue) by marcH (subscriber, #57642) [Link]

I understand that there is a bad looking inconsistency between:
- optimization step 1 computes the result of a signed overflow
- optimization step 2 assumes there is never any signed overflow

I did not like the "loophole" sentence because I (mis?)understood it as: "gcc is punishing everyone who has not read the standard, on purpose".
I mean: the inconsistency between step 1 and step 2 is not evil! It is just an accident that happens to be allowed by the standard and that has benefits in other cases when step 1 and step 2 do not collide that bad.

Sure, but then you deserve what you get...

Posted May 24, 2011 23:14 UTC (Tue) by iabervon (subscriber, #722) [Link]

Actually, I suspect that gcc actually transforms the condition to "val < 0x104030200". How would gcc, a C program, compute the result of a signed overflow without risking crashing? It's more comprehensible as "step 1 assumes that, because there is no signed overflow, the arbitrary-precision value of the expression 0x03020100 + (i*0x04040404) is the value of val; step 2 notices that the condition is always true due to the limited range of the variable."

This also avoids the issue that it would be really hard to determine that "val != 0x04030200" is always true without determining that the code actually hits a signed overflow.

Actually, the first transformed code is probably:

int *value;
int val;

for (val = 0x03020100; val < 0x104030200; val += 0x04040404, value++)
    *value = val;

Eliminating "i" is reasonably likely to be huge on x86 come register allocation, so it's a good optimization if it works. And gcc can assume it works because the programmer can't let signed arithmetic overflow. Of course, at this point, it already doesn't work as expected; the second optimization just makes the assembly confusing. The second optimization is really for "if (size >= 0x100000000) return -EINVAL;" where the programmer cares about a 32-bit limit in code that could be built with either 32-bit or 64-bit ints; in some builds it's important, and in all builds it's correct, but the compiler can eliminate it in cases where it doesn't matter.

Have you seen this page?

Posted May 25, 2011 5:12 UTC (Wed) by khim (subscriber, #9252) [Link]

How would gcc, a C program, compute the result of a signed overflow without risking crashing?

Have you seen this page? Specifically the libraries part? Do you know why GMP, MPFR and MPC are requirements, not options? Actually I think this particular optimization does not use multiprecision arithmetic, but if it's needed in some passes it is available to GCC despite the fact that it's C program.

P.S. Note that is you really want "overflow", not "undefined behavior" you can do that and the fact that GCC is a C program does not stop you.

Have you seen this page?

Posted May 25, 2011 5:44 UTC (Wed) by iabervon (subscriber, #722) [Link]

Those libraries compute the well-defined results of calculations with large numbers; they don't give the result of signed overflow. They aren't going to help for figuring out the results of running (unoptimized, on the target machine):

if (MAXINT + 1 == -MAXINT) printf("Wow, 1's complement!\n");

because that's a matter of processor architecture, not mathematics. And certainly gcc isn't going to try eliciting the undefined behavior itself and replicating it, because the undefined behavior might be "your compiler crashes processing unreachable code", which the C specification doesn't allow.

Have you seen this page?

Posted May 26, 2011 8:27 UTC (Thu) by marcH (subscriber, #57642) [Link]

> Those libraries compute the well-defined results of calculations with large numbers; they don't give the result of signed overflow.

Of course they can do this; the latter is just one modulus operation away from the former.

> because that's a matter of processor architecture, not mathematics.

Surprise: processors architectures are rooted in arithmetics. All of them, even though they differ with each other.

Have you seen this page?

Posted May 27, 2011 9:45 UTC (Fri) by dgm (subscriber, #49227) [Link]

> Surprise: processors architectures are rooted in arithmetics. All of them, even though they differ with each other.

Still not a matter of mathematics: both 1's and 2's compliment are equally correct. The matter is about the choice made by the processor designers, and thus, a matter of processor architecture.

Have you seen this page?

Posted May 27, 2011 14:03 UTC (Fri) by marcH (subscriber, #57642) [Link]

It's a matter of both: 1. Choose the architecture, 2. Apply the maths specific to this architecture.

GMP or else can help for the latter (of course not for the former).

Sure, but then you deserve what you get...

Posted May 25, 2011 1:27 UTC (Wed) by foom (subscriber, #14868) [Link]

Yes, it has optimization benefits. That doesn't mean it's actually a good feature.

Overloading "signedness" with "cannot overflow" makes no sense, it's simply an accident of history.
But back in history, C compilers weren't smart enough to take advantage of the leeway given: they in fact did exhibit implementation-defined behavior, not undefined behavior, in the face of a signed overflow. They acted like the hardware acts upon signed overflow. It's only fairly recently that optimizers have taken advantage of this loophole in the standard that allows them to blow up your program if you have any signed int overflows.

Of course, assuming that an unsigned int cannot overflow would also have optimization benefits! Does it really make sense that a loop gets slower just because you declare the loop counter as an "unsigned int" instead of an "int"?

Sure, but then you deserve what you get...

Posted May 25, 2011 2:48 UTC (Wed) by iabervon (subscriber, #722) [Link]

Actually, if the compiler were forced to consider overflow, it would just have to think a little harder before making the same optimizations. The compiler could determine that 1<<32 / GCD(1<<32, 0x04040404) > 256/64 and thus that the first time i >= 256/64, val == 0x04030200 with unsigned math, and val != 0x04030200 before that. Your loop would have to get slower (or, more likely, take an additional register) if it was going to use the same value in "val" multiple times, simply because it becomes necessary to track another piece of information.

(Also note that it doesn't matter if you declare the loop counter as an "unsigned int"; the nominal loop counter actually gets discarded entirely, in favor of a proxy loop counter, which is what could overflow.)

Sure, but then you deserve what you get...

Posted May 25, 2011 3:31 UTC (Wed) by iabervon (subscriber, #722) [Link]

Actually, if the compiler is forced to consider overflow, it just has to think a little harder before making the same optimizations. The compiler can determine that 1<<32 / GCD(1<<32, 0x04040404) > 256/4 and thus that the first time i >= 256/4, val == 0x04030200 with unsigned math, and val != 0x04030200 before that. Your loop would have to get slower (or, more likely, take an additional register) if it was going to use the same value in "val" multiple times, simply because it becomes necessary to track another piece of information.

(Also note that it doesn't matter if you declare the loop counter as an "unsigned int"; the nominal loop counter actually gets discarded entirely, in favor of a proxy loop counter, which is what could overflow.)

With gcc 4.4.5, replacing "int val" with "unsigned int val" makes it actually generate what you would expect of:

for (val = 0x03020100; val != 0x04030200; val += 0x04040404, value++)
    *value = val;

which avoids the "i = 0", "*value = val" is a simpler instruction than "value[i] = val", and uses one fewer register; but it still actually works. If the constant you're adding is 0x04000000, the optimization doesn't work, and gcc produces the slower code. The code it produces for "unsigned int" is the fastest working code possible, so it's not getting slower in any meaningful way by using "unsigned int" (I mean, it loops faster without testing the end condition, but...).

Sure, but then you deserve what you get...

Posted May 26, 2011 17:21 UTC (Thu) by anton (guest, #25547) [Link]

Does it really make sense that a loop gets slower just because you declare the loop counter as an "unsigned int" instead of an "int"?
It gets slower? Doesn't the code with int loop infinitely when compiled with gcc? Great optimization!

Sure, but then you deserve what you get...

Posted May 28, 2011 20:48 UTC (Sat) by BenHutchings (subscriber, #37955) [Link]

C compilers weren't smart enough to take advantage of the leeway given: they in fact did exhibit implementation-defined behavior, not undefined behavior, in the face of a signed overflow. They acted like the hardware acts upon signed overflow.

Which was to crash, in many cases. Signed overflow caused a processor exception, just like division by zero, because the result could not be represented.

Sure, but then you deserve what you get...

Posted May 28, 2011 20:46 UTC (Sat) by BenHutchings (subscriber, #37955) [Link]

Signed overflow *ought to have been* implementation defined in C from the beginning, not undefined.

Signed overflow results in an exception on some processors. So the range of permissible implementation-defined behaviour would have to include: the program aborts. Slightly better than the current situation, but not much.

Unsigned versus signed

Posted May 24, 2011 12:58 UTC (Tue) by cesarb (subscriber, #6266) [Link]

Wow...

That was an impressive one. I did not expect gcc to use *val* for the loop condition.

I think this is yet one more point in favor of a personal rule of "always use unsigned unless you have a good reason to use signed" (that is, use "unsigned" by default when programming). If you followed that rule, all three "int" on this function would become "unsigned int", since there is no good reason to use signed here.

Sure, but then you deserve what you get...

Posted May 25, 2011 10:14 UTC (Wed) by welinder (guest, #4699) [Link]

That piece of code does not invoke undefined behaviour unless
"int" is too small to hold whatever value things sum up to.

This program may also invoke undefined behaviour:

int main (int argc, char **argv) { return 65535+1; }

Sure, but then you deserve what you get...

Posted May 26, 2011 17:05 UTC (Thu) by anton (guest, #25547) [Link]

Do you really want to punish the compiler and force it to keep "for loop iterative variable" in memory to make sure it's changed via float if the pointers are mixed just right?
Sure, if I keep an induction variable in a global variable, static variable or an auto variable that I take the address of, I expect that variable to be in memory and to be fetched from there and/or stored there whenever I access it.

What's this nonsense about punishing the compiler? It's a thing without feelings, it cannot be punished. If it's a good compiler, it will do what I expect. Unfortunately, the most popular compilers become worse and worse with every release.

I would welcome the world that you dream of where these compilers miscompile every significant program. Then the pain would be so large that we all (well, you and a few others excepted) would finally scratch our itch and write a compiler that does not do any of that nonsense, and gcc etc. would go the way of XFree86, like they should.

Copyright © 2013, Eklektix, Inc.
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds