Po raz pierwszy zauważyłem w 2009 roku, że GCC (przynajmniej w moich projektach i na moich maszynach) ma tendencję do generowania zauważalnie szybszego kodu, jeśli optymalizuję pod kątem rozmiaru ( -Os
) zamiast prędkości ( -O2
lub -O3
), i od tego czasu zastanawiam się, dlaczego.
Udało mi się stworzyć (raczej głupiutki) kod, który pokazuje to zaskakujące zachowanie i jest wystarczająco mały, aby go tutaj opublikować.
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;
}
Jeśli go skompiluję -Os
, wykonanie tego programu zajmie 0,38 s, a 0,43 s, jeśli jest kompilowany z -O2
lub -O3
. Czasy te są uzyskiwane konsekwentnie i praktycznie bez hałasu (gcc 4.7.2, x86_64 GNU / Linux, Intel Core i5-3320M).
(Aktualizacja: przeniosłem cały kod asemblera do GitHub : sprawili, że post był rozdęty i najwyraźniej dodają bardzo mało wartości do pytań, ponieważ fno-align-*
flagi mają taki sam efekt).
Oto wygenerowany zespół za pomocą -Os
i -O2
.
Niestety, moje rozumienie montaż jest bardzo ograniczony, więc nie mam pojęcia, czy to, co zrobiłem obok była prawidłowa: Złapałem zespół do -O2
i połączyła wszystkie swoje różnice w zespole za -Os
wyjątkiem tych .p2align
linii, prowadzić tutaj . Ten kod nadal działa w ciągu 0,38 s, a jedyną różnicą jest to, co jest .p2align
.
Jeśli dobrze się domyślam, są to podkładki do wyrównania stosu. Według Dlaczego podkładka GCC działa z NOP? robi się to z nadzieją, że kod będzie działał szybciej, ale najwyraźniej ta optymalizacja nie powiodła się w moim przypadku.
Czy winowajcą jest winowajca w tym przypadku? Dlaczego i jak?
Hałas, który powoduje, praktycznie uniemożliwia mikrooptymalizacje czasowe.
Jak mogę się upewnić, że takie przypadkowe wyrównania szczęścia / nieszczęścia nie przeszkadzają, gdy przeprowadzam mikrooptymalizacje (niezwiązane z wyrównaniem stosu) w kodzie źródłowym C lub C ++?
AKTUALIZACJA:
Po odpowiedzi Pascala Cuoqa majstrowałem trochę przy ustawieniach. Po przekazaniu -O2 -fno-align-functions -fno-align-loops
do gcc wszystkie .p2align
znikają ze złożenia, a wygenerowany plik wykonywalny działa w 0,38 sekundy. Zgodnie z dokumentacją gcc :
-Os włącza wszystkie optymalizacje -O2 [ale] -Os wyłącza następujące flagi optymalizacji:
-falign-functions -falign-jumps -falign-loops -falign-labels -freorder-blocks -freorder-blocks-and-partition -fprefetch-loop-arrays
Wydaje się więc, że jest to (źle) problem z wyrównaniem.
Nadal jestem sceptyczny, -march=native
co sugeruje odpowiedź Marata Dukhana . Nie jestem przekonany, że to nie tylko przeszkadza w tym (błędnym) problemie z wyrównaniem; nie ma absolutnie żadnego wpływu na moją maszynę. (Niemniej jednak głosowałem za jego odpowiedzią).
AKTUALIZACJA 2:
Możemy -Os
usunąć zdjęcie. Poniższe czasy są uzyskiwane przez kompilację z
-O2 -fno-omit-frame-pointer
0,37s-O2 -fno-align-functions -fno-align-loops
0,37s-S -O2
następnie ręcznie przesuwając zespóładd()
powork()
0,37 sekundy-O2
0,44s
Wygląda na to, że odległość add()
od strony połączeń ma duże znaczenie. Próbowałem perf
, ale wyniki perf stat
i perf report
nie mają dla mnie większego sensu. Mogłem jednak uzyskać tylko jeden spójny wynik:
-O2
:
602,312,864 stalled-cycles-frontend # 0.00% frontend cycles idle
3,318 cache-misses
0.432703993 seconds time elapsed
[...]
81.23% a.out a.out [.] work(int, int)
18.50% a.out a.out [.] add(int const&, int const&) [clone .isra.0]
[...]
¦ __attribute__((noinline))
¦ static int add(const int& x, const int& y) {
¦ return x + y;
100.00 ¦ lea (%rdi,%rsi,1),%eax
¦ }
¦ ? retq
[...]
¦ int z = add(x, y);
1.93 ¦ ? callq add(int const&, int const&) [clone .isra.0]
¦ sum += z;
79.79 ¦ add %eax,%ebx
Dla fno-align-*
:
604,072,552 stalled-cycles-frontend # 0.00% frontend cycles idle
9,508 cache-misses
0.375681928 seconds time elapsed
[...]
82.58% a.out a.out [.] work(int, int)
16.83% a.out a.out [.] add(int const&, int const&) [clone .isra.0]
[...]
¦ __attribute__((noinline))
¦ static int add(const int& x, const int& y) {
¦ return x + y;
51.59 ¦ lea (%rdi,%rsi,1),%eax
¦ }
[...]
¦ __attribute__((noinline))
¦ static int work(int xval, int yval) {
¦ int sum(0);
¦ for (int i=0; i<LOOP_BOUND; ++i) {
¦ int x(xval+sum);
8.20 ¦ lea 0x0(%r13,%rbx,1),%edi
¦ int y(yval+sum);
¦ int z = add(x, y);
35.34 ¦ ? callq add(int const&, int const&) [clone .isra.0]
¦ sum += z;
39.48 ¦ add %eax,%ebx
¦ }
Dla -fno-omit-frame-pointer
:
404,625,639 stalled-cycles-frontend # 0.00% frontend cycles idle
10,514 cache-misses
0.375445137 seconds time elapsed
[...]
75.35% a.out a.out [.] add(int const&, int const&) [clone .isra.0] ¦
24.46% a.out a.out [.] work(int, int)
[...]
¦ __attribute__((noinline))
¦ static int add(const int& x, const int& y) {
18.67 ¦ push %rbp
¦ return x + y;
18.49 ¦ lea (%rdi,%rsi,1),%eax
¦ const int LOOP_BOUND = 200000000;
¦
¦ __attribute__((noinline))
¦ static int add(const int& x, const int& y) {
¦ mov %rsp,%rbp
¦ return x + y;
¦ }
12.71 ¦ pop %rbp
¦ ? retq
[...]
¦ int z = add(x, y);
¦ ? callq add(int const&, int const&) [clone .isra.0]
¦ sum += z;
29.83 ¦ add %eax,%ebx
Wygląda na to, że opóźniamy połączenie do add()
wolnej sprawy.
Sprawdziłem wszystko , coperf -e
może wypluć na moją maszynę; nie tylko statystyki podane powyżej.
Dla tego samego pliku wykonywalnego stalled-cycles-frontend
pokazuje liniową korelację z czasem wykonania; Nie zauważyłem nic innego, co by tak wyraźnie korelowało. (Porównywanie stalled-cycles-frontend
różnych plików wykonywalnych nie ma dla mnie sensu.)
Jako pierwszy komentarz uwzględniłem brakujące dane z pamięci podręcznej. Sprawdziłem wszystkie błędy pamięci podręcznej, które można zmierzyć na mojej maszynie perf
, a nie tylko te podane powyżej. Błędy w pamięci podręcznej są bardzo bardzo głośne i wykazują niewielką lub żadną korelację z czasami wykonania.