gotchacppCritical
Why does GCC generate 15-20% faster code if I optimize for size instead of speed?
Viewed 0 times
gccfasterwhyinsteadforgeneratedoesoptimizespeedsize
Problem
I first noticed in 2009 that GCC (at least on my projects and on my machines) have the tendency to generate noticeably faster code if I optimize for size (
I have managed to create (rather silly) code that shows this surprising behavior and is sufficiently small to be posted here.
If I compile it with
(Update: I have moved all assembly code to GitHub: They made the post bloated and apparently add very little value to the questions as the
Here is the generated assembly with
Unfortunately, my understanding of assembly is very limited, so I have no idea whether what I did next was correct: I grabbed the assembly for
If I guess correctly, these are paddings for stack alignment. According to Why does GCC pad functions with NOPs? it is done in the hope that the code will run faster, but apparently this optimization backfired in my case.
Is it the padding that is the culprit in this case? Why and how?
The noise it makes pretty much makes timing micro-optimizations imp
-Os) instead of speed (-O2 or -O3), and I have been wondering ever since why.I have managed to create (rather silly) code that shows this surprising behavior and is sufficiently small to be posted here.
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;
}If I compile it with
-Os, it takes 0.38 s to execute this program, and 0.44 s if it is compiled with -O2 or -O3. These times are obtained consistently and with practically no noise (gcc 4.7.2, x86_64 GNU/Linux, Intel Core i5-3320M).(Update: I have moved all assembly code to GitHub: They made the post bloated and apparently add very little value to the questions as the
fno-align-* flags have the same effect.)Here is the generated assembly with
-Os and -O2.Unfortunately, my understanding of assembly is very limited, so I have no idea whether what I did next was correct: I grabbed the assembly for
-O2 and merged all its differences into the assembly for -Os except the .p2align lines, result here. This code still runs in 0.38s and the only difference is the .p2align stuff.If I guess correctly, these are paddings for stack alignment. According to Why does GCC pad functions with NOPs? it is done in the hope that the code will run faster, but apparently this optimization backfired in my case.
Is it the padding that is the culprit in this case? Why and how?
The noise it makes pretty much makes timing micro-optimizations imp
Solution
My colleague helped me find a plausible answer to my question. He noticed the importance of the 256 byte boundary. He is not registered here and encouraged me to post the answer myself (and take all the fame).
Short answer:
Is it the padding that is the culprit in this case? Why and how?
It all boils down to alignment. Alignments can have a significant impact on the performance, that is why we have the
I have submitted a (bogus?) bug report to the gcc developers. It turns out that the default behavior is "we align loops to 8 byte by default but try to align it to 16 byte if we don't need to fill in over 10 bytes." Apparently, this default is not the best choice in this particular case and on my machine. Clang 3.4 (trunk) with
Of course, if an inappropriate alignment is done, it makes things worse. An unnecessary / bad alignment just eats up bytes for no reason and potentially increases cache misses, etc.
The noise it makes pretty much makes timing micro-optimizations
impossible.
How can I make sure that such accidental lucky / unlucky alignments
are not interfering when I do micro-optimizations (unrelated to stack
alignment) on C or C++ source codes?
Simply by telling gcc to do the right alignment:
Long answer:
The code will run slower if:
-
an
-
if the call to
-
if
-
if the loop is not aligned.
The first 2 are beautifully visible on the codes and results that Marat Dukhan kindly posted. In this case,
a 256 byte boundary cuts
In case
Nothing is aligned, and the call to
In case
This is the fastest of all three. Why the 256 byte boundary is speacial on his machine, I will leave it up to him to figure it out. I don't have such a processor.
Now, on my machine I don't get this 256 byte boundary effect. Only the function and the loop alignment kicks in on my machine. If I pass
I first noticed in 2009 that gcc (at least on my projects and on my
machines) have the tendency to generate noticeably faster code if I
optimize for size (-Os) instead of speed (-O2 or -O3) and I have been
wondering ever since why.
A likely explanation is that I had hotspots which were sensitive to the alignment, just like the one in this example. By messing with the flags (passing
Oh, and one more thing. How can such hotspots arise, like the one shown in the example? How can the inlining of such a tiny function like
Consider this:
and in a separate file:
and
Short answer:
Is it the padding that is the culprit in this case? Why and how?
It all boils down to alignment. Alignments can have a significant impact on the performance, that is why we have the
-falign-* flags in the first place.I have submitted a (bogus?) bug report to the gcc developers. It turns out that the default behavior is "we align loops to 8 byte by default but try to align it to 16 byte if we don't need to fill in over 10 bytes." Apparently, this default is not the best choice in this particular case and on my machine. Clang 3.4 (trunk) with
-O3 does the appropriate alignment and the generated code does not show this weird behavior.Of course, if an inappropriate alignment is done, it makes things worse. An unnecessary / bad alignment just eats up bytes for no reason and potentially increases cache misses, etc.
The noise it makes pretty much makes timing micro-optimizations
impossible.
How can I make sure that such accidental lucky / unlucky alignments
are not interfering when I do micro-optimizations (unrelated to stack
alignment) on C or C++ source codes?
Simply by telling gcc to do the right alignment:
g++ -O2 -falign-functions=16 -falign-loops=16Long answer:
The code will run slower if:
-
an
XX byte boundary cuts add() in the middle (XX being machine dependent).-
if the call to
add() has to jump over an XX byte boundary and the target is not aligned.-
if
add() is not aligned.-
if the loop is not aligned.
The first 2 are beautifully visible on the codes and results that Marat Dukhan kindly posted. In this case,
gcc-4.8.1 -Os (executes in 0.994 secs):00000000004004fd :
4004fd: 8d 04 37 lea eax,[rdi+rsi*1]
400500: c3a 256 byte boundary cuts
add() right in the middle and neither add() nor the loop is aligned. Surprise, surprise, this is the slowest case!In case
gcc-4.7.3 -Os (executes in 0.822 secs), the 256 byte boundary only cuts into a cold section (but neither the loop, nor add() is cut):00000000004004fa :
4004fa: 8d 04 37 lea eax,[rdi+rsi*1]
4004fd: c3 ret
[...]
40051a: e8 db ff ff ff call 4004fa Nothing is aligned, and the call to
add() has to jump over the 256 byte boundary. This code is the second slowest.In case
gcc-4.6.4 -Os (executes in 0.709 secs), although nothing is aligned, the call to add() doesn't have to jump over the 256 byte boundary and the target is exactly 32 byte away:4004f2: e8 db ff ff ff call 4004d2
4004f7: 01 c3 add ebx,eax
4004f9: ff cd dec ebp
4004fb: 75 ec jne 4004e9 This is the fastest of all three. Why the 256 byte boundary is speacial on his machine, I will leave it up to him to figure it out. I don't have such a processor.
Now, on my machine I don't get this 256 byte boundary effect. Only the function and the loop alignment kicks in on my machine. If I pass
g++ -O2 -falign-functions=16 -falign-loops=16 then everything is back to normal: I always get the fastest case and the time isn't sensitive to the -fno-omit-frame-pointer flag anymore. I can pass g++ -O2 -falign-functions=32 -falign-loops=32 or any multiples of 16, the code is not sensitive to that either.I first noticed in 2009 that gcc (at least on my projects and on my
machines) have the tendency to generate noticeably faster code if I
optimize for size (-Os) instead of speed (-O2 or -O3) and I have been
wondering ever since why.
A likely explanation is that I had hotspots which were sensitive to the alignment, just like the one in this example. By messing with the flags (passing
-Os instead of -O2), those hotspots were aligned in a lucky way by accident and the code became faster. It had nothing to do with optimizing for size: These were by sheer accident that the hotspots got aligned better. From now on, I will check the effects of alignment on my projects.Oh, and one more thing. How can such hotspots arise, like the one shown in the example? How can the inlining of such a tiny function like
add() fail?Consider this:
// add.cpp
int add(const int& x, const int& y) {
return x + y;
}and in a separate file:
// main.cpp
int add(const int& x, const int& y);
const int LOOP_BOUND = 200000000;
__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;
}and
Code Snippets
00000000004004fd <_ZL3addRKiS0_.isra.0>:
4004fd: 8d 04 37 lea eax,[rdi+rsi*1]
400500: c300000000004004fa <_ZL3addRKiS0_.isra.0>:
4004fa: 8d 04 37 lea eax,[rdi+rsi*1]
4004fd: c3 ret
[...]
40051a: e8 db ff ff ff call 4004fa <_ZL3addRKiS0_.isra.0>4004f2: e8 db ff ff ff call 4004d2 <_ZL3addRKiS0_.isra.0>
4004f7: 01 c3 add ebx,eax
4004f9: ff cd dec ebp
4004fb: 75 ec jne 4004e9 <_ZL4workii+0x13>// add.cpp
int add(const int& x, const int& y) {
return x + y;
}// main.cpp
int add(const int& x, const int& y);
const int LOOP_BOUND = 200000000;
__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;
}Context
Stack Overflow Q#19470873, score: 217
Revisions (0)
No revisions yet.