Why does GCC generate 15-20% faster code if I optimize for size instead of speed?

Why does GCC generate 15-20% faster code if I optimize for size instead of speed? ๐ค
Have you ever noticed that when you optimize your code with GCC, specifically when you choose the -Os optimization flag for size, the generated code tends to run noticeably faster (around 15-20%) than when you optimize for speed using the -O2 or -O3 flags? ๐ฎ
You're not alone! This intriguing behavior has been observed by many developers, and today we're going to explore why this happens and what you can do to leverage this optimization technique to improve the performance of your code. ๐ช
The Surprising Performance Boost ๐
Let's take a look at a simple example code that showcases this behavior:
const int LOOP_BOUND = 200000000;
__attribute__((noinline))
static int add(const int& x, const int& y) {
return x + y;
}
__attribute__((noinline))
static int work(int xval, int yval) {
int sum(0);
for (int i = 0; i < LOOP_BOUND; ++i) {
int x(xval + sum);
int y(yval + sum);
int z = add(x, y);
sum += z;
}
return sum;
}
int main(int , char* argv[]) {
int result = work(*argv[1], *argv[2]);
return result;
}When we compile this code with -Os, it executes in 0.38 s. However, if we compile it with -O2 or -O3, it takes 0.44 s. These execution times are consistent and reproducible across different machines.
Unraveling the Mystery ๐ง
The first question that comes to mind is: why does optimizing for size result in faster code execution? ๐คทโโ๏ธ
To understand this behavior, we need to take a closer look at the generated assembly code. By examining the assembly code differences between the -Os and -O2 optimizations, we can pinpoint what's causing the discrepancy in performance.
But hold on! Understanding assembly code can be quite tricky, especially for enthusiasts with limited experience in this area (like me ๐โโ๏ธ). So, let's focus on the key differences rather than diving into all the nitty-gritty details.
One specific difference that stands out is the presence of .p2align lines in the assembly code generated with -O2 but not in that generated with -Os. These lines are related to stack alignment and are intended to optimize performance. However, in this particular case, it appears that this optimization has a reverse effect, leading to slower execution times.
A (Mis)alignment Issue ๐คจ
If we assume that these .p2align lines are responsible for the performance difference, we can deduce that they introduce padding for stack alignment. This padding, intended to improve code execution speed, seems to have a negative impact in this particular scenario.
To confirm this hypothesis, we can examine the impact of alignment by compiling with additional flags: -fno-align-functions and -fno-align-loops. These flags disable the aligning optimization and eliminate the .p2align lines from the assembly code.
Surprisingly, when we compile the code with these flags alongside -O2, the execution time matches that of the optimized size version -Os, i.e., 0.38 s. This confirms that the (mis)alignment issue is the culprit behind the slower execution.
Micro-Optimizations and Accidental Alignments ๐ต๏ธโโ๏ธ
But wait, there's more! Now that we understand the alignment issue, we face another challenge: accidental lucky or unlucky alignments interfering with micro-optimizations unrelated to stack alignment. How can we ensure that these accidental alignments don't hinder our efforts to improve code performance?
To mitigate this problem, we should focus on techniques that minimize misalignment. One approach is to pass the -fno-omit-frame-pointer flag alongside -O2. This flag helps align the code better, resulting in improved performance without the negative impact caused by the (mis)alignment issue.
Additionally, using the -S flag followed by manually moving the assembly of the add() function after the work() function can also eliminate the discrepancy in execution times. By keeping the call to add() closer to the call site, we reduce the effect of accidental alignments, leading to faster code execution.
Wrap Up and Take Action ๐
To sum it up, when optimizing with GCC, using the -Os flag for size can result in significantly faster code execution (around 15-20%) compared to optimizing for speed with the -O2 or -O3 flags. This happens due to an (un)alignment issue caused by the introduction of padding for stack alignment, which has a reverse effect on performance.
To make sure accidental lucky or unlucky alignments don't interfere with your micro-optimizations, consider using the -fno-omit-frame-pointer flag alongside -O2 and manually reordering your assembly code by placing critical functions closer to their call sites. These techniques can eliminate the negative impact of misalignment and improve the performance of your code.
So go ahead, give it a try! Optimize your code for size, leverage the power of (mis)alignment, and see your code soar to new performance heights! โจ
Have you experienced similar performance discrepancies when optimizing your code? Share your insights and join the discussion in the comments below! ๐
Take Your Tech Career to the Next Level
Our application tracking tool helps you manage your job search effectively. Stay organized, track your progress, and land your dream tech job faster.



