Search code examples
cgccgcc-warning

Is gcc's optimizer doing something weird here?


I have a program that triggers -Wmaybe-uninitialized while using any kind of optimizations (-O1, -O2, -O3) the following is the smallest code I could make that reproduces this behavior.

#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>

struct result {
    int res;
};

struct graph {
    int vertices[5];
};

// In reality this is a backtracking search.
void computational_search (struct graph *g, struct result *out) {
    out->res = 0;
    int i;
    for (i=0; i<5; i++) {
        out->res += g->vertices[i];
    }
}

// In reality queries a database of geometrical graphs.
void next_graph (struct graph *g)
{
    int i;
    for (i=0; i<5; i++) {
        g->vertices[i] += rand();
    }
}

enum format_t {
    FMT = 1,
    FMT_1 = 2
};

int main()
{
    int val = rand()%10;
    int num_graphs = 5;

    struct graph g;
    struct result res;

    uint64_t *count;
    if (val & FMT) {
        count = malloc(sizeof(uint64_t)*num_graphs);
    }

    int i;
    for (i=0; i<num_graphs; i++) {
        next_graph (&g);
        computational_search (&g, &res);
        if (val & FMT) {
            count[i] = res.res; /* ERROR HERE */
        }
    }
    return 0;
}

I'm aware there is an execution path where count is uninitialized, but it's not being used there. Can the optimizer be doing something that may use count uninitialized?.

Compilig with gcc -Wall -O1 test.c outputs this on gcc 5.4.0:

test.c: In function ‘main’:
test.c:43:15: warning: ‘count’ may be used uninitialized in this function [-Wmaybe-uninitialized]
     uint64_t *count;
               ^

The same happens for -O2 and -O3, but not for -O0.


Solution

  • Let me say it a bit bluntly: -Wmaybe-uninitialized sucks. What GCC actually tries to do is first perform a lot of optimizing transformations and then check that every use of a variable is preceded by an initialization (hoping that the optimizer is smart enough to get all impossible paths eliminated). IIRC it also has a couple of simple heuristics. With such an approach you should expect false positives.

    For example at -O3 the compiler should unswitch the loop in main and transform it into something like this:

    if (val & FMT) {
        for (i=0; i<num_graphs; i++) {
            next_graph (&g);
            computational_search (&g, &res);
            count[i] = res.res;
        }
    } else {
        for (i=0; i<num_graphs; i++) {
            next_graph (&g);
            computational_search (&g, &res);
        }
    }
    

    Then it should notice that we already have a val & FMT check and merge the corresponding branches:

    if (val & FMT) {
        count = malloc(sizeof(uint64_t)*num_graphs);
        for (i=0; i<num_graphs; i++) {
            next_graph (&g);
            computational_search (&g, &res);
            count[i] = res.res;
        }
    } else {
        for (i=0; i<num_graphs; i++) {
            next_graph (&g);
            computational_search (&g, &res);
        }
    }
    

    The path involving an uninitialized use is gone. Now, back to the reality: unswitching does not happen (for some reason). If you add -fno-inline (note: it's just a test, I'm not proposing it as a solution), it does happen and everything works as I described earlier (no warning). IMHO, this warning is very brittle and unpredictable.

    Can the optimizer be doing something that may use count uninitialized?.

    No, unless it has a bug.