84

I have come across the below C++ program (source):

#include <iostream>
int main()
{
    for (int i = 0; i < 300; i++)
        std::cout << i << " " << i * 12345678 << std::endl;
}

It looks like a simple program and gives the correct output on my local machine i.e. something like:

0 0
1 12345678
2 24691356
...
297 -628300930
298 -615955252
299 -603609574

But, on online IDEs like codechef, it gives the following output:

0 0
1 12345678
2 24691356
...
4167 -95167326
4168 -82821648
4169 -7047597

Why doesn't the for loop terminate at 300? Also this program always terminates on 4169. Why 4169 and not some other value?

arpanmangal
  • 1,770
  • 1
  • 17
  • 34

4 Answers4

109

I'm going to assume that the online compilers use GCC or compatible compiler. Of course, any other compiler is also allowed to do the same optimization, but GCC documentation explains well what it does:

-faggressive-loop-optimizations

This option tells the loop optimizer to use language constraints to derive bounds for the number of iterations of a loop. This assumes that loop code does not invoke undefined behavior by for example causing signed integer overflows or out-of-bound array accesses. The bounds for the number of iterations of a loop are used to guide loop unrolling and peeling and loop exit test optimizations. This option is enabled by default.

This option merely allows making assumptions based on cases where UB is proven. To take advantage of those assumptions, other optimizations may need to be enabled, such as constant folding.


Signed integer overflow has undefined behaviour. The optimizer was able to prove that any value of i greater than 173 would cause UB, and because it can assume that there is no UB, it can also assume that i is never greater than 173. It can then further prove that i < 300 is always true, and so the loop condition can be optimized away.

Why 4169 and not some other value?

Those sites probably limit the number of output lines (or characters or bytes) they show and happen to share the same limit.

Community
  • 1
  • 1
eerorika
  • 232,697
  • 12
  • 197
  • 326
  • Doesn't the local g++ compiler does similar optimisation? – arpanmangal Feb 11 '18 at 12:43
  • 1
    @ArpanMangal it sure does. Did you enable optimisation? O2 Level appears to be needed. – eerorika Feb 11 '18 at 12:52
  • 3
    If the compiler can prove that there will be UB for any value of `i` greater than 173, so why doesn't it emit a warning instead of doing a pointless optimisation? – Jabberwocky Feb 11 '18 at 12:52
  • 7
    @MichaelWalz It does emit one. – HolyBlackCat Feb 11 '18 at 12:56
  • @HolyBlackCat OK thanks, that wasn't clear in the OP's question. But here I don't get any warning: https://godbolt.org/g/VouEqH – Jabberwocky Feb 11 '18 at 13:00
  • 1
    @MichaelWalz Huh. `-O2` makes it emit one, but `-faggressive-loop-optimizations` doesn't. – HolyBlackCat Feb 11 '18 at 13:06
  • 1
    @HolyBlackCat actually just using `-faggressive-loop-optimizations` apparently doesn't do anything, the generated code is the same – Jabberwocky Feb 11 '18 at 13:12
  • @user2079303 I compiled earlier without the optimisations on my machine. With the -O2 flag it gives a warning on compilation as Ron pointed out and as output it just keeps printing way beyond 4169 which shows that online IDEs truncate the output. – arpanmangal Feb 11 '18 at 13:12
  • 3
    @MichaelWalz: Removing redundant boolean tests is not a pointless optimization; it is a very useful one. What *would* be (mostly) pointless is adding extra code to disable/undo the optimization in the presence of broken source code. –  Feb 11 '18 at 14:20
  • 25
    @MichaelWalz: The compiler can't reliably detect UB as you suggest (though it can sometimes warn on a probable occurrence, as it actually _does_ here). Instead, it can proceed with best intentions _on the assumption that there will not be UB_. Those are two perhaps subtly but in fact substantially different things. It's not like the compiler went "aha! UB! now I can turn on _such and such an optimisation_" — the optimisation was always there. It's doing stuff like this _all the time_. But, as long as your program is correctly written, you do not witness any changes to its would-be semantics. – Lightness Races in Orbit Feb 12 '18 at 00:23
  • 20
    As an analogy, the manufacturer of your house's front door may have decided that it'd be more robust if it had a piece of metal tactically placed somewhere in the middle of it. You're never going to notice, unless you're punching holes in your door and thus _using doors wrong_. – Lightness Races in Orbit Feb 12 '18 at 00:27
  • Which architecture was used to 'get' the problem? I noticed that the gcc assembly for C (same prog, replace iostream w printf) was easier to read and much shorter than C++. Did not see the removed loop condition even w optimizations turned on, neither for MIPS or x86. – Erik Alapää Feb 12 '18 at 12:09
  • @ErikAlapää x86_64, GCC 7.2, C++ language, `-O2` reproduced the behaviour observed by OP. – eerorika Feb 12 '18 at 12:26
  • @LightnessRacesinOrbit Or installing a cat flap - in which case the metal can be a concern because it interferes with the chip reader. – Taemyr Feb 12 '18 at 13:23
  • @user2079303 on https://godbolt.org, both gcc 7.2 and g++ 7.2 give the UB warning when using -O2, but only the C++ compiler seems to remove the loop condition when using -O2. (C version uses printf instead of cout). – Erik Alapää Feb 12 '18 at 13:45
  • UPDATE: Actually, even C++ compiler does not optimize out loop cond when using printf, weird... – Erik Alapää Feb 12 '18 at 14:03
  • @Taemyr: That should be a part of the specified semantics of your door. – Lightness Races in Orbit Feb 12 '18 at 14:29
  • @LightnessRacesinOrbit: So the fact that a lockbox will spring open if someone bonks it just right shouldn't be considered a defect, since any thief who bonks it in such fashion and steals the contents wouldn't be "using it properly"? Quality language implementations intended for use with programs that process data from untrustworthy sources should try to help programmers limit what a malicious data supplier would be able to accomplish--not help the malicious data supplier by optimizing out safety checks that guard against critical UB in cases that would otherwise invoke Annex-L "bounded UB". – supercat Apr 30 '18 at 22:37
  • @supercat: Quality of implementation is something else. Also you keep quoting "bounded UB" from C but this is C++ – Lightness Races in Orbit Apr 30 '18 at 22:52
  • @LightnessRacesinOrbit: Yeah, I know C and C++ have diverged, though most principles that are useful for one should be useful for both. – supercat Apr 30 '18 at 23:09
  • @supercat you can always go looking for lockbox vendors who precisely define what happens when you "bonk it" (i.e. check for and throw exceptions) – Caleth Jun 13 '18 at 13:46
  • @Caleth: The issue isn't with the vendor of the lockbox, but rather those supplying the manufacturing and design tools. A good lockbox should offer multiple layers of redundant security. While it may be advantageous to omit certain bits of redundancy that would be incapable of doing anything under any circumstances, it is not advantageous to omit bits of redundancy which would become relevant in cases where multiple components have been subjected to stresses beyond their rated limits, and some have failed but others haven't. – supercat Jun 13 '18 at 15:03
44

"Undefined behaviour is undefined." (c)

A compiler used on codechef seems to use following logic:

  1. Undefined behaviour can't happen.
  2. i * 12345678 overflows and results in UB if i > 173 (assuming 32 bit ints).
  3. Thus, i can never exceed 173.
  4. Thus i < 300 is superfluous and can be replaced with true.

The loop itself appears to be infinite. Apparently, codechef just stops the program after a specific amount of time or truncates the output.

HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207
  • But why does it terminate on 4169 for both codechef and ideone? – arpanmangal Feb 11 '18 at 12:28
  • 11
    @ArpanMangal I just counted the characters in output, and it seems to be `2^16`. Apparently that's a coincidence that both truncate output to `2^16` characters. – HolyBlackCat Feb 11 '18 at 12:39
  • 3
    @ArpanMangal: nasal demons like 4169, and are currently partying at ideone and codechef. UB is undefined, it can be anything, including nasal demons. In all seriousness, trying to analyze UB is a waste of time, use that time to figure out how to keep it from happening. – jmoreno Feb 11 '18 at 15:09
  • 6
    @jmoreno it's not a waste of time if you are interested in compiler design – user253751 Feb 11 '18 at 22:50
  • 2
    @jmoreno Though, this time it was possible to analyze it. Understanding how exactly UB breaks stuff might be useful to conclude in which cases UB is acceptable, if any. – HolyBlackCat Feb 11 '18 at 23:01
  • 1
    @immibis: I don't see how. The compiler is acting 100% correctly by definition. It can give an error, launch nukes, emit a program that erases your hardrive, it can randomly switch between doing those things. Anything it does is correct. – jmoreno Feb 11 '18 at 23:26
  • 1
    I would say that it was useful to identify that UB was happening and why. How it breaks...that is up to the compiler. UB means the compiler no longer needs to be deterministic in order to be correct. – jmoreno Feb 11 '18 at 23:33
  • 4
    @jmoreno Anything the universe does is correct (by definition), so what is the point in studying astronomy? – user253751 Feb 11 '18 at 23:43
12

You are invoking undefined behavior probably on 174th iteration inside your for loop as the max int value probably is 2147483647 yet 174 * 123456789 expression evaluates to 2148147972 which is undefined behavior as there is no signed integer overflow. So you are observing effects of UB, particularly with the GCC compiler with optimization flags set in your case. It is likely the compiler would have warned you about this by issuing the following warning:

warning: iteration 174 invokes undefined behavior [-Waggressive-loop-optimizations]

Remove the (-O2) optimization flags to observe different results.

Ron
  • 14,674
  • 4
  • 34
  • 47
  • 5
    It is worth noting that undefined behavior can have *retroactive* effects -- because UB would happen on iteration 174, the standard doesn't even require that the first 173 iterations of the loop should proceed as expected! –  Feb 11 '18 at 14:10
  • @Hurkyl Indeed. It's somewhat paradoxical that UB causes the entire program, including the previous statements to exhibit UB. – Ron Feb 11 '18 at 14:13
  • 12
    @Ron: It's not paradoxical. Either the program has well-defined semantics, or it doesn't. Period. Remember, C++ code is not a sequence of instructions to be carried out; it is _a description of a program_. – Lightness Races in Orbit Feb 11 '18 at 18:15
  • @LightnessRacesinOrbit: It is possible for a program to have semantics which are partially unspecified but not totally undefined. The C Standard attempts to apply that concept to what it calls "Bounded UB", though the language it uses is a bit vague. Allowing compilers broad but not unlimited freedom in handling things like integer overflow wouldn't interfere with many otherwise-useful optimizations, but would make the language much more suitable for processing data received from untrustworthy sources. – supercat Apr 30 '18 at 22:44
  • @supercat: Indeed, unspecified behaviour is not the same as undefined behaviour. We are discussing the latter – Lightness Races in Orbit Apr 30 '18 at 22:51
  • Although I don't know that the C++ Standard has such a feature, the C Standard defines a category of implementations that define `__STDC_ANALYZABLE__` to a non-zero value. For such implementations, the Standard distinguishes "Bounded UB" and "Critical UB", and attempts to limit the effects of the former. – supercat Apr 30 '18 at 23:04
7

The compiler can assume that undefined behavior will not occur, and because signed overflow is UB, it can assume that never i * 12345678 > INT_MAX, thus also i <= INT_MAX / 12345678 < 300 and therefore remove the check i < 300.

yassin
  • 642
  • 2
  • 7
  • 18