6

I've come accross a very strange behavior in gcc regarding operators and functions marked with __attribute((const)). Logical and arithmetic operators lead to different optimizations, and I don't understand why.

It's not really a bug since __attribute((const)) is only a hint and there's no guarantee to its effect, but still this is very surprising. Anyone has any explanation ?

Here's the code. So I define an __attribute((const)) function:

int f(int & counter) __attribute((const));
int f(int & counter) {
    ++counter;
    return 0;
}

Then I define an operator testing macro. This is done with macros and not templates/functors to present simple code to the compiler and simplify the optimization:

int global = 0; // forces results to be computed

#define TestOp(OP) \
    { \
        int n = 0; \
        global += (f(n) OP f(n)); \
        std::cout << "op" #OP " calls f " << n << " times" << std::endl; \
    }

And finally, I test different operators as follows. The comments match the output with g++-4.8 -std=c++11 -O2 -Wall -pedantic same output at -O3 and -Ofast

int main() {
    // all calls optimized away
    TestOp(^)   // 0
    TestOp(-)   // 0
    // one call is optimized away
    TestOp(|)   // 1
    TestOp(&)   // 1
    TestOp(||)  // 1
    TestOp(&&)  // 1
    // no optimization
    TestOp(+)   // 2
    TestOp(*)   // 2

    return global;
}

My question is: why do arithmetic operators yield two calls? Why couldn't f()+f() be optimized as 2*f() ? Is there a way to help/force this optimization ? At first I thought multiplication might be more expensive, but I tried with f()+....+f() and 10 additions still don't reduce to 10*f(). Also, since it's int arithmetic, operation order is irrelevant (contrary to floats).

I also checked the asm but it doesn't help: all ints seem to be pre-computed at compile-time.

Antoine
  • 13,494
  • 6
  • 40
  • 52
  • you sould maybe try it with O3 – PlasmaHH Dec 02 '13 at 13:41
  • @PlasmaHH: doesn't change – Antoine Dec 02 '13 at 13:42
  • The docs warns that a const function must not inspect pointer arguments, i'd presume that would be the same for references (and let alone also change any of the arguments passed in - const is supposed to be even stricter than the pure attribute). – nos Dec 02 '13 at 13:49
  • @nos Well, obviously there's no "legal" means to debug a const function, so I'm stuck with this. But that souldn't be an issue since these attributes are not checked and the compiler is supposed to trust me about it. – Antoine Dec 02 '13 at 13:51
  • what happens when you remove the __attribute((const)) from the function , or when you put the function inside a class member where you can put `const` directly? – Raxvan Dec 02 '13 at 14:04
  • 2
    @Antonie Well, if you make your function `int f(int i) __attribute((const));` , place the implementation in a different compilation unit (where it alters a global variable to keep track of the count), I get quite different results - only TestOp(|) performs 1 call, the others are optimized away, which would be the expected case when calling f(0). – nos Dec 02 '13 at 14:04
  • As mentioned by `nos` the issue might be that the function call is inlined before being elided. – Matthieu M. Dec 02 '13 at 14:09
  • @nos Indeed, putting it in another compilation unit changes results! – Antoine Dec 02 '13 at 14:18
  • Well, the code is UB, so anything the compiler does is legitimate. `f(n)` modifies its argument, so `f(n) OP f(n)` modifies the same value twice without a sequence point in between. It's like writing `++n+++n` or something similar. – Damon Dec 02 '13 at 14:47
  • @Damon It's a function call, so there will be a sequence point, there's no such issue. You can't be certain which of the left hand or right hand side is called first though. – nos Dec 02 '13 at 16:54

1 Answers1

5

The compiler doesn't trust you. Since you have a reference argument, the compiler doesn't seem to trust your const attribute - a const function is supposed to only look at values passed through the arguments (not references or dereferencing pointers).

Another way to test this is to break the const function out in a separate compilation unit:

test1.cpp:

#include <stdio.h>
int global = 0; // forces results to be computed

int f(int i) __attribute((const));
void print_count(void);

#define TestOp(OP) \
    { \
        int n = 0; \
        global += (f(n) OP f(n)); \
        printf("op %s ", #OP);\
        print_count();\
    }

int main() {
    // all calls optimized away
    TestOp(^)   // 0
    TestOp(-)   // 0
    // one call is optimized away
    TestOp(|)   // 1
    TestOp(&)   // 1
    TestOp(||)  // 1
    TestOp(&&)  // 1
    // no optimization
    TestOp(+)   // 2
    TestOp(*)   // 2

    return global;
}

counter.cpp:

#include <stdio.h>
static int counter = 0;

int f(int i) {
    ++counter;
    return 0;
}

void print_count(void)
{
   printf("counter %d\n", counter);
    counter = 0;
}

Now the compiler figures out that there's no need to call f(0) until f(0) | f(0), and the result of that one call to f(0) is re-used for the other cases.

$ g++ -O2 -c counter.cpp && g++ -O2 -c test.cpp && g++ counter.o test.o && ./a.out
op ^ counter 0
op - counter 0
op | counter 1
op & counter 0
op || counter 0
op && counter 0
op + counter 0
op * counter 0
nos
  • 223,662
  • 58
  • 417
  • 506
  • Thanks! So the compiler only "trusts" me if it's in another compilation unit ? When it's in the same unit, wouldn't it make sense to issue a warning when the compiler removed my attribute annotation ? – Antoine Dec 02 '13 at 14:20
  • But what's the deal with `f(0) | f(0)`? I don't see why it should be any different – harold Dec 02 '13 at 14:26
  • @harold I changed it, it's not special - it's just the first time the compiler needs to call f(0). To be able to test all operators seperatly, you'd have to call f(n) with a different n for each operator though. – nos Dec 02 '13 at 14:29
  • 1
    I think it is good in this case that the compiler doesn't trust him. To expand on the explanation, the reason it works in a separate compilation unit is that at the call site, the compiler can't see the contents (since it is in another compilation unit), so it is more willing to believe the annotation, since it has no reason not to. – Tim Seguine Dec 02 '13 at 20:52