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 ( -O2lub -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 -O2lub -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ą -Osi -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 -O2i połączyła wszystkie swoje różnice w zespole za -Os wyjątkiem tych .p2alignlinii, 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-loopsdo gcc wszystkie .p2alignznikają 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=nativeco 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 -Osusunąć zdjęcie. Poniższe czasy są uzyskiwane przez kompilację z
-O2 -fno-omit-frame-pointer0,37s-O2 -fno-align-functions -fno-align-loops0,37s-S -O2następnie ręcznie przesuwając zespóładd()powork()0,37 sekundy-O20,44s
Wygląda na to, że odległość add()od strony połączeń ma duże znaczenie. Próbowałem perf, ale wyniki perf stati perf reportnie 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-frontendpokazuje liniową korelację z czasem wykonania; Nie zauważyłem nic innego, co by tak wyraźnie korelowało. (Porównywanie stalled-cycles-frontendróż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.