The compiler has several optimization passes. Each optimization step is responsible for a number of small optimizations. For example, you might have a skip that calculates arithmetic expressions at compile time (so you can express 5 MB as 5 * (1024 * 1024) without a penalty, for example). Another pass of built-in functions. Another searches for unreachable code and kills it. And so on.
Then the compiler developers decide which of these passes they want to execute in which order. For example, suppose you have this code:
int foo(int a, int b) { return a + b; } void bar() { if (foo(1, 2) > 5) std::cout << "foo is large\n"; }
If you delete this code, nothing happens. Similarly, if you perform expression reduction, nothing happens. But inliner can decide that foo is small enough to be inline, so it replaces the call in the bar with the body of the function, replacing the arguments:
void bar() { if (1 + 2 > 5) std::cout << "foo is large\n"; }
If you are now performing expression reduction, you must first decide that 1 + 2 is 3, and then decide that 3> 5 is false. So you get:
void bar() { if (false) std::cout << "foo is large\n"; }
And now fixing the dead code will see if (false) and kill it, so the result is:
void bar() { }
But now the bar is suddenly very tiny when it was bigger and more complex. Therefore, if you run inliner again, it will be able to integrate the bar into its callers. This may provide even more room for optimization, etc.
For compiler developers, this is a trade-off between compilation time and generated code quality. They determine the startup sequence of optimizers based on heuristics, testing, and experience. But since one size does not fit everyone, they set some knobs to adjust. The main handle for gcc and clang is the -O option family. -O1 launches a short list of optimizers; -O3 launches a much longer list containing more expensive optimizers, and retries occur more often.
In addition to determining which optimizers work, parameters can also tune the internal heuristics used by various passes. For example, inliner, as a rule, there are many parameters that determine when to embed a function. Pass-O3, and these parameters will be more oriented towards the built-in functions when there is a chance of increasing productivity; pass -Os, and parameters will cause only tiny functions (or functions supposedly called exactly once) to be embedded, since everything else will increase the size of the executable file.