If a programmer writes "X+1 > X", chances are this is an overflow check. Doing this is perfectly defined by the standard if X is an unsigned integer, but not if it's signed. That's what doesn't make sense, since they could've made the unsigned case undefined as well.
which allows a broad range of loop optimizations to kick in
What sort of optimisations exactly, and just how significant are they? I don't believe C should be a language where the compiler does all sorts of high-level optimisation; it should be a straightforward "do what I say" type of language where you get almost exactly what you write, and the only optimisations should be at the level of things like instruction selection --- the optimisations that a programmer would not be able to do at the source level.
In that case, you're asking for easily 3x-5x less performance in the software you use every day, if not more. You can get essentially this by turning on -O0 and compiling, say, Firefox. This is done in debugging, and it's a terrible browsing experience.
Compiler optimizations are really, really important. CSE, GVN, LICM, SROA, etc. aren't done for fun. They're done because modern software depends on them to be fast.
Writing low-level "post-optimized" code is at odds with good engineering practice. If you can't factor things out into functions and count on an inliner to inline them and then propagate away constants, SROA away intermediate structures, etc. then you're essentially telling programmers they can't factor code out into functions. If your compiler refuses to do the Hacker's Delight integer divide optimization (which, by the way, you need good constprop to make good use of a lot of the time), then you're telling programmers they have to get out their copy from their bookshelf and compute the magic number every time they want to integer divide fast (especially on ARM). This sort of thing puts a huge drain on developer productivity and maintainability.
There is no such thing as "+ on x86". "+" is an operator of the C language. Modern x86 has about two dozen instructions that can be used to perform additions, each with slightly different behaviour, and with different performance characteristics. Someone has to define how C's "+" maps to those instructions. That is what the C standard does. The way it does that is by specifying properties that any implementation of C's "+" needs to have, leaving the specific choice of instructions to the compiler writer.
But the C standard doesn't say "+ maps to what you could honestly call plain integer addition" anyway, so it's kindof pointless? And if you were to define your own language, you obviously could define "+" to have architecture-specific semantics, whithout appealing to any "plain integer addition".
I'm not defining it that way. I used the word 'plain' because some instructions are specifically designed to be variants of normal instructions. It's in their name that they do thing abnormally, so they should be discarded when talking about normal behavior.
>But the C standard doesn't say "+ maps to what you could honestly call plain integer addition" anyway, so it's kindof pointless?
You originally said: There is no such thing as "+ on x86". "+" is an operator of the C language.
So I was explaining what "+" means in the absence of C rules.
+ means addition in general conversation, that's basic math and English skills.
That it's integer addition is obvious from context, because we're adding 1 to INT_MAX.
And I already explained why I used the word 'plain'.
-
There are architectures with normal integer add instructions that do not have twos-complement behavior. I'm just not aware of any such instructions on x86. Can you name one?
If there are none, then I am comfortable asserting that "+" on x86 exists and is twos-complement.
Well ... yeah, "+" usually means addition, sure. But there are so many types of addition that that's not really specific enough for a language definition. And adding 1 to INT_MAX? Well, you could interpret "INT_MAX" to indicate C code, and thus "+" to be C's addition operator. In which case, it's not integer addition, at least not the kind that basic math and English skills would suggest: Adding two positive integers is always defined (and also never gives a value lower than either of the two operands), very much unlike "+" for "int"s in C.
> There are architectures with normal integer add instructions that do not have twos-complement behavior. I'm just not aware of any such instructions on x86. Can you name one?
Sure, PADDSW, for example. At least if I understand your "twos complement" requirement correctly to mean a specific wraparound behaviour. Or, if you want to count that, DAA and AAA.
As I see it that's a variant of PADDW, so whatever PADDW does is what matters, and PADDW does twos complement.
>DAA and AAA
I thought about calling those out, I think it's stretching too much to refer to an archaic instruction that works on 'ascii' or such when we're figuring the behavior of 'integers'. Even if it is one byte.
Nope, PADDW is a variant of PADDSW, so whatever PADDSW does is what matters, and PADDSW does saturating addition.
Or in other words: You are defining "normal" to mean "twos-complement with wrap-around" after all, which makes it rather unsurprising that every "normal" instruction according to your definition turns out to be doing twos-complement addition with wrap-around.
> I thought about calling those out, I think it's stretching too much to refer to an archaic instruction that works on 'ascii' or such when we're figuring the behavior of 'integers'. Even if it is one byte.
Well, packed BCD definitely is not ASCII. Nor twos-complement, nor ones-complement, nor IEEE754. And BCD quite definitely represents a range of the integers.
> Nope, PADDW is a variant of PADDSW, so whatever PADDSW does is what matters, and PADDSW does saturating addition.
That's like arguing that Red Sports Car is a variant of Red Sports Car with Stripes. The one that adds adjectives is the variant, the one without those adjectives is the base.
If PADDW did saturating arithmetic and PADDWW did wrapping, I would agree with you, but that's not how x86 works.
So the original original quote, "on x86 INT_MAX+1==INT_MIN", is true. As far as I can tell.
Which says more about Firefox than it does about -O0. I've run Seamonkey on 3x-5x slower machines than I use these days and it's been fine.
It's common to write off compiler optimizations as unimportant, because they're invisible and people don't see them. They're also complex, which makes people predisposed to get rid of them in the name of "simplicity". But, for better or for worse, optimizing compilers are necessary complexity.
Optimizing compilers are not ubiquitous because compiler engineers just like to play with technology. They're ubiquitous because you need them.
http://www.amazon.com/Computer-Architecture-Fifth-Edition-Qu...
http://www.cecs.pdx.edu/~alaa/ece587/papers/patterson_isca_1...
I do agree that current processors optimize for C/C++ (although of course there are niche systems like Azul which optimize for other languages). It would be nice to have processor extensions that allow us get better GC performance, or better handling of immutable values. There's a chicken-and-egg problem getting there.
On GPU's the binary compatibility issue is solved by having the driver compile the shader/compute kernel before it's used. As an example nvidia uses PTX (see http://docs.nvidia.com/cuda/parallel-thread-execution/) as an intermediate language in CUDA which is then compiled by the runtime into the actual ASM.
On modern CPU's the register renaming has already decoupled the physical registers from the instruction set register. As an example modern haswell has over 100 registers per core.
Would you mind expanding this.
So, in general I am saying that the road to better performance will not be in aggressive compiler optimization, but rather in higher level design tools to manage totally new software/hardware abstractions. Binaries will be specified at a higher level and look more like source code. At this point my crystal ball becomes admittedly a bit fuzzy.
Take the canonical optimizer-created security hole: the hardware optimizer replaces a constant-time compare (which doesn't leak timing information) with a variable-time compare (which does).
I don't think this solves the problem we're setting out to solve, ie. the optimizer introducing bugs.
A more amusing issue I saw was an optimization that looked for places where it could replace manual memory copying with ca call to memcpy. Something that drove the guys writing libc nutzoid. Because it was replacing the code in memcpy with a call to memcpy. (On some platforms you can implement memcpy with special assembly language calls. On some you can't)
Personally I care little about speed, since if I need more speed I can get that. And frankly if you tell me the resulting binary is 20% faster for some things, I just do not care. But I worry a lot about losing the ability to reason about side effects.
Right now compiler writers are playing in a kind of local minimum (premature optimization as I said.) This may produce 3x-5x faster binary code today but also forces CPU manufacturers to retain backward compatibility causing them to also stay stuck in this local well. Eventually a new architecture is created (with 10x the registers etc.) and the cycle continues.
Well, if you could remove one instruction per loop on most computers (especially conditionals that could cause a branch mis-prediction), the effect of that in terms of performance is pretty staggering. Instead of thinking of it as an optimization, think of it as a check that doesn't need to be inserted into the code. If you can tell the compiler "I promise I won't overflow this variable", that's a lot less error checking it needs to insert on every iteration.
> I don't believe C should be a language where the compiler does all sorts of high-level optimisation; it should be a straightforward "do what I say" type of language where you get almost exactly what you write
Well, you could just compile with -O0 then. I don't necessarily agree with C on this, but it's pretty clear the community has made the decision to prioritize speed over safety. Nothing wrong with that, but if safety is your priority you should probably look at things other than C. Even if you were to define undefined behavior, it's a spectacularly dangerous language.
And those things are?
C is the low level language we have. Now compiler writers decided it'll work like a high level language, except for all the actual benefits.
We'll probably get usable C compilers that work (in fact, this already started), and some stuff only good for old code.
If the programmer writes
for (i = a; i < b; i++)
then it's not clear if you need to check 'i' for overflow; Assuming that 'i++' is greater than 'i' allows the compiler to, say, unroll the loop.It does make sense if you understand the historical context: There was a time when some processors still used ones-complement representation for signed integers. That's why the highest performance choice of instructions for signed integer addition would give different results in the overflow case on different processors. Which is why the standard leaves the behaviour undefined, so that compiler writers can choose the instructions that make for the fastest signed addition on the respective target, while programmers can not rely on any specific behaviour.
None of that applies for unsigned integers, and also, the common overflow behaviour of unsigned integers is commonly used intentionally for many types of computations (such as in cryptography), so it is both useful and it doesn't cost anything in terms of performance to define the semantics of unsigned overflow.
Here's a good example from the llvm blog: ( http://blog.llvm.org/2011/05/what-every-c-programmer-should-... )
> Signed integer overflow: If arithmetic on an 'int' type (for example) overflows, the result is undefined. One example is that "INT_MAX+1" is not guaranteed to be INT_MIN. This behavior enables certain classes of optimizations that are important for some code. For example, knowing that INT_MAX+1 is undefined allows optimizing "X+1 > X" to "true". Knowing the multiplication "cannot" overflow (because doing so would be undefined) allows optimizing "X*2/2" to "X". While these may seem trivial, these sorts of things are commonly exposed by inlining and macro expansion. A more important optimization that this allows is for "<=" loops like this:
> for (i = 0; i <= N; ++i) { ... }
> In this loop, the compiler can assume that the loop will iterate exactly N+1 times if "i" is undefined on overflow, which allows a broad range of loop optimizations to kick in. On the other hand, if the variable is defined to wrap around on overflow, then the compiler must assume that the loop is possibly infinite (which happens if N is INT_MAX) - which then disables these important loop optimizations.