Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Do compilers reduce simple functions given constant arguments into unique instructions?

This is something I've always thought to be true but have never had any validation. Consider a very simple function:

int subtractFive(int num) {
    return num -5;
}

If a call to this function uses a compile time constant such as

  getElement(5);

A compiler with optimizations turned on will very likely inline this. What is unclear to me however, is if the num - 5 will be evaluated at runtime or compile time. Will expression simplification extend recursively through inlined functions in this manner? Or does it not transcend functions?

like image 770
Thomas Avatar asked Dec 02 '22 16:12

Thomas


2 Answers

We can simply look at the generated assembly to find out. This code:

int subtractFive(int num) {
    return num -5;
}

int main(int argc, char *argv[]) {
  return subtractFive(argc);
}

compiled with g++ -O2 yields

leal    -5(%rdi), %eax
ret

So the function call was indeed reduced to a single instruction. This optimization technique is known as inlining.

One can of course use the same technique to see how far a compiler will go with that, e.g. the slightly more complicated

int subtractFive(int num) {
    return num -5;
}

int foo(int i) {
    return subtractFive(i) * 5;
}

int main(int argc, char *argv[]) {
  return foo(argc);
}

still gets compiled to

leal    -25(%rdi,%rdi,4), %eax
ret

so here both functions where just eliminated at compile time. If the input to foo is known at compile time, the function call will (in this case) simply be replaced by the resulting constant at compile time (Live).

The compiler can also combine this inlining with constant folding, to replace the function call with its fully evaluated result if all arguments are compile time constants. For example,

int subtractFive(int num) {
    return num -5;
}

int foo(int i) {
    return subtractFive(i) * 5;
}

int main() {
  return foo(7);
}

compiles to

mov     eax, 10
ret

which is equivalent to

int main () {
    return 10;
}

A compiler will always do this where it thinks it is a good idea, and it is (usually) way better in optimizing code on this low level than you are.

like image 176
Baum mit Augen Avatar answered Feb 01 '23 23:02

Baum mit Augen


It's easy to do a little test; consider the following

int foo(int);
int bar(int x) { return x-5; }
int baz() { return foo(bar(5)); }

Compiling with g++ -O3 the asm output for function baz is

xorl    %edi, %edi
jmp _Z3fooi

This code loads a 0 in the first parameter and then jumps into the code of foo. So the code from bar is completely disappeared and the computation of the value to pass to foo has been done at compile time.

In addition returning the value of calling the function became just a jump to the function code (this is called "tail call optimization").

like image 39
6502 Avatar answered Feb 02 '23 00:02

6502