0

I have the following C++ code. It should print 1. There is only one illegal memory access and it is a reading one, i.e., for i=5 in the for loop, we obtain c[5]=cdata[5] and cdata[5] does not exist. Still, all writing calls are legal, i.e., the program never tries to write to some non-allocated array cell. If I compile it with g++ -O3, it prints 0. If I compile without -O3 it prints 1. It's the strangest error I've ever seen with g++.

Later edit to address some answers. The undefined behavior should be limited to reading cdata[5] and writing it to c[5]. The assignment c[5]=cdata[5] may read some unknown garbage or something undefined from cdata[5]. Ok, but that should only copy the garbage to c[5] without writing anything whatsoever somewhere else!

#include <iostream>
#include <cstdlib>
using namespace std;
int n,d;
int *f, *c;
void loadData(){
    int fdata[] = {7,  2, 2, 7, 7, 1};
    int cdata[] = {66, 5, 4, 3, 2};
    n = 6;
    d = 3;
    f = new int[n+1];
    c = new int[n];
    f[0] = fdata[0];
    c[0] = cdata[0];
    for (int i = 1;i<n;i++){
        f[i] = fdata[i];
        c[i] = cdata[i];
    }
    cout<<f[5]<<endl;
}
int main(){
    loadData();
}
Daniel Porumbel
  • 299
  • 2
  • 6
  • 5
    Going out of bounds of an array (or other memory) leads to *undefined behavior*. With UB you could experience something like you do, you could have a crash, or your computer could explode, or your cat might turn into a dog. In short: Undefined behavior is *bad* and you should avoid it at all cost. – Some programmer dude Mar 04 '21 at 20:34
  • 6
    You've answered your own question, really: You have undefined behavior - read access to non-allocated memory - and your problem happens when compiling with optimizations. Duh! – davidbak Mar 04 '21 at 20:34
  • 2
    With an array, you are only allowed to access the elements in it. Going outside it bounds means you've broken the contract with the language and anything you get is "correct" – NathanOliver Mar 04 '21 at 20:35
  • 3
    Here's a [great 3-part blog series you really should read](https://blog.regehr.org/archives/213) - actually, every C/C++ programmer should read it. _I speak from experience!_ (BTW: Those blog posts are from 2010 ... compilers have gotten only _more aggressive_ about exploiting "undefined behavior" since then!!) – davidbak Mar 04 '21 at 20:37
  • 2
    Broken code often behaves differently when compiled with optimizations on and off. – Martin York Mar 04 '21 at 20:38
  • Related: [Can a C/C++ program seg-fault from reading past the end of an array (UNIX)?](https://stackoverflow.com/q/7261037/6372809) – ilkkachu Mar 04 '21 at 20:54
  • 1
    I edited the question. The undefined behavior should be limited to reading `cdata[5]` and to writing on `c[5]`. The assignment `c[5]=cdata[5]` may read some unknown garbage or something undefined from `cdata[5]`. Ok, but that should only copy the garbage to `c[5]` without writing anything whatsoever somewhere else! – Daniel Porumbel Mar 05 '21 at 09:16
  • 1
    I agree, Daniel. It is not the mere reading at an unallocated memory address that causes the issue. Furthermore you had specified that it is only caused with the `-O3` optimisation option. In fact, it is the loop refactoring strategy given by `-O -fpeel-loops` that will refactor your loop and giving the error. More details in my answer below – BiOS Mar 05 '21 at 09:49

4 Answers4

2

It will be very hard to find out exactly what is happening to your code before and after the optimisation. As you already knew and pointed out yourself, you are trying to go out of bound of an array, which leads to undefined behaviour.

However, you are curious on why it is (apparently) the -O3 flag which "causes" the issue!

Let's start by saying that it is actually the flag -O -fpeel-loops which is causing your code to re-organise in a way that your error becomes apparent. The -O3 will enable several optimisation flags, among which -O -fpeel-loops.

You can read here about what the compiler flags are at which stage of optimisation.

In a nutshell, -fpeel-loops wheel re-organise the loop, so that the first and last couple of iterations are actually taken out of the loop itself, and some variables are actually cleared of memory. Small loops may even be taken apart completely!

With this said and considered, try running this piece of code, with -O -fpeel-loops or even with -O3:

#include <iostream>
#include <cstdlib>
using namespace std;
int n,d;
int *f, *c;
void loadData(){
    int fdata[] = {7,  2, 2, 7, 7, 1};
    int cdata[] = {66, 5, 4, 3, 2};
    n = 6;
    d = 3;
    f = new int[n+1];
    c = new int[n];
    f[0] = fdata[0];
    c[0] = cdata[0];
    for (int i = 1;i<n;i++){
        cout << f[i];
        f[i] = fdata[i];
        c[i] = cdata[i];
    }
    cout << "\nFINAL F[5]:" << f[5]<<endl;
}
int main(){
    loadData();
}

You will see that it will print 1 regardless of your optimisation flags.

That is because of the statement: cout << f[i], which will change the way that fpeel-loops is operating.

Also, experiment with this block of code:

f[0] = fdata[0];
c[0] = cdata[0];

c[1] = cdata[1];
c[2] = cdata[2];
c[3] = cdata[3];
c[4] = cdata[4];

for (int i = 1; i<n; i++) {
    f[i] = fdata[i];
    c[5] = cdata[5];
}

cout << "\nFINAL F[5]:" << f[5] <<endl;

You will see that even in this case, with all your optimisation flags, the output is 1 and not 0. Even in this case:

for (int i = 1; i<n; i++) {
   c[i] = cdata[i];
}

for (int i = 1; i<n; i++) {
   f[i] = fdata[i];
}

The produced output is actually 1. This is, again, because we have changed the structure of the loop, and fpeel-loops is not able to reorganise it as before, in the way that the error was produced. It's also the case of wrapping it into a while loop:

int i = 1;
while (i < 6) {
    f[i] = fdata[i];
    c[i] = cdata[i];
    i++;
}

Although on a while loop -O3 will prevent compilation here because of -Waggressive-loop-optimizations, so you should test it with -O -fpeel-loops

So, we can't really know for sure how your compiler is reorganising that loop for you, however it is using the so-called as-if rule to do so, and you can read more about it here.

Of course, your compiler takes the freedom o refactoring that code for you basing on the fact that you abide to set rules. The as-if rule for the compiler will always produce the desired output, providing that the programmer has not caused undefined behaviour. In our case, we do indeed have broken the rules by going out of bounds of that array, hence the strategy with which that loop was refactored, was built upon the idea that this could not happen, but we made it happen.

So yes, it is not everything as simple and straightforward as saying that all your problems are created by reading at an unallocated memory address. Of course, it is ultimately why the problem has verfied itself, but you were reading cdata out of bounds, which is a different array! So it is not as simple and easy as saying that the mere reading out of bounds of your array is causing the issue, as if you do it like that:

int i = 0;

f[i] = fdata[i];
c[i] = cdata[i];
i++;

f[i] = fdata[i];
c[i] = cdata[i];
i++;

f[i] = fdata[i];
c[i] = cdata[i];
i++;

f[i] = fdata[i];
c[i] = cdata[i];
i++;

f[i] = fdata[i];
c[i] = cdata[i];
i++;

f[i] = fdata[i];
c[i] = cdata[i];
i++;

It will work and print 1! Even if we are definitely going out of bounds with that cdata array! So it is not the mere fact of reading at an unallocated memory address that is causing the issue.

Once again, it is actually the loop-refactoring strategy of fpeel-loops, which believed that you would not go out-of-bounds of that array and changed your loop accordingly.

Lastly, the fact that optimisations flags will lead you to have an output of 0 is strictly related to your machine. That 0 has no meaning, is is not the product of an actual logical operation, because it is actually a junk value, found at a non allocated memory address, or the product of a logical operation performed on a junk value, resulting in NaN.

BiOS
  • 2,282
  • 3
  • 10
  • 24
  • Thanks for all theses tests. Yes, it's probably the loop refactoring strategy of `-O -fpeel-loops` that is too aggressive. If I reduce `n` up to 3, the program prints `0`. For `n=2` it prints `1`. I also tried to replace `n` with `3` everywhere, and it prints `1`. I needed some time to read&understand all your answer and I still need to think about the ``as-if'' rule. – Daniel Porumbel Mar 06 '21 at 14:45
  • You are very welcome, and it was indeed very interesting to go over the problem that you described, yours was a very good question! I wouldn't say that `fpeel-loops` is being too aggressive, but it is just re-arranging the loop in a way that makes the data at `cdata[5]` break the rest of the operation, maybe it is matching something between the `fdata` and `cdata` arrays... It really is a mistery, but if you are creative enough you may even find out how it actually re-arranged the loop so that the program prints 0! – BiOS Mar 06 '21 at 15:20
0

If you want to discover why this happens, you can examine the generated assembly code. Here it is in Compiler Explorer: https://godbolt.org/z/bs7ej7

Note that with -O3, the generated assembly code is not exactly easy to read.

Greg Hewgill
  • 951,095
  • 183
  • 1,149
  • 1,285
  • See [the As-if Rule](https://en.cppreference.com/w/cpp/language/as_if) for a bit more information about that last paragraph. Since the compiler can do whatever it wants to make the code faster, the output often looks like you ran the source code through a blender. – user4581301 Mar 04 '21 at 20:58
0

As you mentioned, you have memory problem in your code.

"cdata" has 5 member and maximum index for it is 4. But in for loop when "i = 5", invalid data is read and it could cause a undefined behavior.

  c[i] = cdata[i];

Please modify your code like this and try again.

    for (int i = 1; i < n; i++)
        f[i] = fdata[i];
    for (int i = 1; i < 5; i++)
        c[i] = cdata[i];
0

The answer is simple - you are accessing memory out of bounds of the array. Who knows what is there? It could be some other variable in your program, it could be random garbage, it is completely undefined.

So, when you compile sometimes you just so happen to get data there that is valid and other times when you compile the data is not what you expect.

It is purely coincidental that the data you expect is there or not, and depending on what the compiler does will determine what data is there i.e. it is completely unpredictable.

As for memory access, reading invalid memory is still a big no-no. So is reading potentially uninitialized values. You are doing both. They may not cause a segmentation fault, but they are still big no-no's.

My recommendation is use valgrind or a similar memory checking tool. If you run this code through valgrind, it will yell at you. With this, you can catch memory errors in your code that the compiler might not catch. This memory error is obvious, but some are hard to track down and almost all lead to undefined behavior, which makes you want to pull your teeth out with a rusty pair of pliers.

In short, don't access out of bounds elements and pray that the answer you are looking for just so happens to exist at that memory address.

Lucas Streanga
  • 469
  • 2
  • 7
  • 1
    It's not quite so straightforward as this. The out of bounds read access happens on a *different* unrelated array to the one where the wrong result appears. – Greg Hewgill Mar 05 '21 at 01:04
  • I like the way you start. `c[5]=cdata[5]` reads some "unknown garbage" from `cdata[5]`. But that should only copy the garbage to `c[5]` without writing anything whatsoever somewhere else. – Daniel Porumbel Mar 05 '21 at 09:07