10

Do compilers (generally or in particular) optimize repeated function calls?

For example, consider this case.

struct foo {
  member_type m;
  return_type f() const; // returns by value
};

The function definition is in one translation unit

return_type foo::f() const {
  /* do some computation using the value of m */
  /* return by value */
}

Repeated function calls are in another unit

foo bar;

some_other_function_a(bar.f());
some_other_function_b(bar.f());

Would the code in the second translation unit be converted to this?

foo bar;

const return_type _tmp_bar_f = bar.f();

some_other_function_a(_tmp_bar_f);
some_other_function_b(_tmp_bar_f);

Potentially, the computation f does can be expensive, but the returned type can be something very small (think about a mathematical function returning a double). Do compilers do this? Are there cases when they do or don't? You can consider a generalized version of this question, not just for member functions, or functions with no arguments.

Clarification per @BaummitAugen's suggestion:

I'm more interested in the theoretical aspect of the question here, and not so much in whether one could rely on this to make real world code run faster. I'm particularly interested in GCC on x86_64 with Linux.

12
  • 1
    Try it and see ... Commented Feb 9, 2017 at 1:04
  • Honest advice: Just measure if it makes a difference. If you can't measure a difference, it's not significant. Commented Feb 9, 2017 at 1:05
  • @M.M Well, that would only test a few particular scenarios that I can think of. Also, I don't really understand assembly. Commented Feb 9, 2017 at 1:06
  • No unless the function is defined pure (having no side effects) in a non-standard way. Commented Feb 9, 2017 at 1:07
  • 1
    @SU3 Ok, upvoted and starred. I can't look into this right now, but if there is no good answer to this by tomorrow, I'll look into it. Commented Feb 9, 2017 at 1:41

3 Answers 3

7

GCC absolutely optimizes across compilation units if you have Link Time Optimization on and the optimization level is high enough, see here: https://gcc.gnu.org/wiki/LinkTimeOptimization There is really no reason besides compilation time to not do both of these.

Additionally, you can always help the compiler along by marking the function with the appropriate attributes. You probably want to mark the function with the attribute const as follows:

struct foo {
  member_type m;
  return_type f() const __attribute__((const)); // returns by value
};

Take a look at GCCs documentation here to see which attribute is appropriate: https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html

In a more general sense, this is very easy for a compiler to detect. It actually performs transformations that are much less obvious. The reason why Link Time Optimization is important, though, is that once GCC has generated actual machine code, it will not really know what is safe at that point to do. Your function could, for example, modify data (outside your class) or access a volatile variable.

EDIT:

GCC most definitely can do this optimization. With this code and the flags -O3 -fno-inline:

C++ code:

#include <iostream>

int function(int c){
  for(int i = 0; i != c; ++i){
    c += i;
  }
  return c;
}

int main(){
  char c;
  ::std::cin >> c;
  return function(c) + function(c) + function(c) + function(c) + function(c);
}

Assembly Output:

4006a0: 48 83 ec 18             sub    rsp,0x18
4006a4: bf 80 0c 60 00          mov    edi,0x600c80
4006a9: 48 8d 74 24 0f          lea    rsi,[rsp+0xf]
4006ae: e8 ad ff ff ff          call   400660 <_ZStrsIcSt11char_traitsIcEERSt13basic_istreamIT_T0_ES6_RS3_@plt>
4006b3: 0f b6 7c 24 0f          movzx  edi,BYTE PTR [rsp+0xf]
4006b8: e8 13 01 00 00          call   4007d0 <_Z8functioni>
4006bd: 48 83 c4 18             add    rsp,0x18
4006c1: 8d 04 80                lea    eax,[rax+rax*4]
4006c4: c3                      ret    
4006c5: 66 66 2e 0f 1f 84 00    data32 nop WORD PTR cs:[rax+rax*1+0x0]
4006cc: 00 00 00 00 

It does, however, fail to do this when the function is in a separate compilation unit and the -flto option is not specified. Just to clarify, this line calls the function:

call   4007d0 <_Z8functioni>

And this line multiplies the result by 5 (adding together five copies):

lea    eax,[rax+rax*4]
Sign up to request clarification or add additional context in comments.

2 Comments

So, are you saying that GCC tries to prove that a function is pure even without attributes? Do you know what optimization option needs to be on, besides lto?
@SU3 There's two ways that I can see it happening. First, is that GCC might inline the function (-finline-functions) and then do common sub-expression elimination (-fgcse). It could, also, do as you suggest and mark the function correctly on its own. I'm not familiar and can't find the mechanism by which it does this right now, but I've read about that.
0

The compiler can't see across compilation units, so it can't tell at the call site whether the call has side-effects, so it would be incorrect to optimize it away.

3 Comments

Depends. LTO can to some extend.
In principle, can pure attribute be specified on the function declaration to allow the compiler to assume that no side effects occur? E.g. [[ gnu::pure ]].
@BaummitAugen Sure, but the question is about the compller.
0

Unless the function and all functions between the first and the last call to the function are declared pure (i.e. not having any side effects), the compiler cannot optimize the calls. Observe the following:

int test();
void some(int a);
void more(int b);

int main()
{
    some(test());
    more(test());
}

Here, test will likely be called twice because it can return different value (LTO can optimize this by inlining quote: “simple enough” functions). If you want the compiler to be able to optimize the calls, it needs to know that both test and some are pure, i.e. caling test for more(test()) cannot possibly return different value than when calling test for some(test()). The following can therefore be optimized (and will in GCC and Clang) to a single call to test:

int test() __attribute__ ((pure));
void some(int a) __attribute__ ((pure));
void more(int b);

int main()
{
    some(test());
    more(test());
}

(Notice that more does not need to be pure.)

Unfortunately, there is not yet any standard way to declare a function as pure, the above is a non-standard GCC extension. There is proposal N3744 to add [[pure]] to ISO C++ (with even stronger guarantees for purity, some would not need to be pure under that) but I don't know if it will make it to C++17 or not.

6 Comments

Does anyone know to what extent GCC can determine if a function is pure if the attribute is not provided?
"Here, test is always guaranteed to be called exactly twice" False. Most linkers are smart enough to inline, at which point it can rearrange the code to call 1 or even 0 times.
@Mooing Duck Can you name such linker? I tested this code with GCC and Clang and neither does it.
@StenSoft: I bet both do it. Change the body of test() to {return 3;} and see how many times it gets "called".
@MooingDuck OK, I have manager to make it to a point where it works like that, the calls are optimized to 1 call. (-flto is not enabled by default even on -O3, that's what made my original test not capture this) Good to learn something new!
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.