Dlaczego GCC generuje kod o 15-20% szybszy, jeśli optymalizuję rozmiar zamiast prędkości?


445

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-pointer 0,37s

  • -O2 -fno-align-functions -fno-align-loops 0,37s

  • -S -O2następnie ręcznie przesuwając zespół add()po work()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 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.


36
Ślepy zgadnij: czy może to być chybiony cache?

@ H2CO3 To była moja pierwsza myśl, ale nie byłem wystarczająco zachęcany do opublikowania komentarza bez dokładnego przeczytania i zrozumienia pytania OP.
πάντα ῥεῖ

2
@ g-makulik Dlatego ostrzegłem, że to „ślepa próba” ;-) „TL; DR” jest zarezerwowane na złe pytania. : P

3
Po prostu interesujący punkt danych: uważam, że -O3 lub -Ofast jest około 1,5 razy szybszy niż -Os, kiedy kompiluję to z clang na OS X. (Nie próbowałem reprodukować z gcc.)
Rob Napier,

2
To jest ten sam kod. Przyjrzyj się bliżej adresowi .L3, źle ustawione cele oddziałów są drogie.
Hans Passant,

Odpowiedzi:


505

Domyślnie kompilatory optymalizują pod kątem „przeciętnego” procesora. Ponieważ różne procesory preferują różne sekwencje instrukcji, optymalizacje kompilatora włączone przez -O2mogą być korzystne dla przeciętnego procesora, ale zmniejszają wydajność konkretnego procesora (i to samo dotyczy -Os). Jeśli spróbujesz tego samego przykładu na różnych procesorach, przekonasz się, że na niektórych z nich skorzystasz, -O2podczas gdy inne są bardziej korzystne dla -Osoptymalizacji.

Oto wyniki dla time ./test 0 0kilku procesorów (raportowany czas użytkownika):

Processor (System-on-Chip)             Compiler   Time (-O2)  Time (-Os)  Fastest
AMD Opteron 8350                       gcc-4.8.1    0.704s      0.896s      -O2
AMD FX-6300                            gcc-4.8.1    0.392s      0.340s      -Os
AMD E2-1800                            gcc-4.7.2    0.740s      0.832s      -O2
Intel Xeon E5405                       gcc-4.8.1    0.603s      0.804s      -O2
Intel Xeon E5-2603                     gcc-4.4.7    1.121s      1.122s       -
Intel Core i3-3217U                    gcc-4.6.4    0.709s      0.709s       -
Intel Core i3-3217U                    gcc-4.7.3    0.708s      0.822s      -O2
Intel Core i3-3217U                    gcc-4.8.1    0.708s      0.944s      -O2
Intel Core i7-4770K                    gcc-4.8.1    0.296s      0.288s      -Os
Intel Atom 330                         gcc-4.8.1    2.003s      2.007s      -O2
ARM 1176JZF-S (Broadcom BCM2835)       gcc-4.6.3    3.470s      3.480s      -O2
ARM Cortex-A8 (TI OMAP DM3730)         gcc-4.6.3    2.727s      2.727s       -
ARM Cortex-A9 (TI OMAP 4460)           gcc-4.6.3    1.648s      1.648s       -
ARM Cortex-A9 (Samsung Exynos 4412)    gcc-4.6.3    1.250s      1.250s       -
ARM Cortex-A15 (Samsung Exynos 5250)   gcc-4.7.2    0.700s      0.700s       -
Qualcomm Snapdragon APQ8060A           gcc-4.8       1.53s       1.52s      -Os

W niektórych przypadkach możesz złagodzić efekt niekorzystnych optymalizacji, prosząc gcco optymalizację pod kątem konkretnego procesora (używając opcji -mtune=nativelub -march=native):

Processor            Compiler   Time (-O2 -mtune=native) Time (-Os -mtune=native)
AMD FX-6300          gcc-4.8.1         0.340s                   0.340s
AMD E2-1800          gcc-4.7.2         0.740s                   0.832s
Intel Xeon E5405     gcc-4.8.1         0.603s                   0.803s
Intel Core i7-4770K  gcc-4.8.1         0.296s                   0.288s

Aktualizacja: na Core i3 z Ivy Bridge trzy wersje gcc( 4.6.4, 4.7.3i 4.8.1) produkują pliki binarne o znacząco różnej wydajności, ale kod asemblera ma tylko subtelne odmiany. Jak dotąd nie mam wyjaśnienia tego faktu.

Montaż z gcc-4.6.4 -Os(wykonuje się w 0,709 sek.):

00000000004004d2 <_ZL3addRKiS0_.isra.0>:
  4004d2:       8d 04 37                lea    eax,[rdi+rsi*1]
  4004d5:       c3                      ret

00000000004004d6 <_ZL4workii>:
  4004d6:       41 55                   push   r13
  4004d8:       41 89 fd                mov    r13d,edi
  4004db:       41 54                   push   r12
  4004dd:       41 89 f4                mov    r12d,esi
  4004e0:       55                      push   rbp
  4004e1:       bd 00 c2 eb 0b          mov    ebp,0xbebc200
  4004e6:       53                      push   rbx
  4004e7:       31 db                   xor    ebx,ebx
  4004e9:       41 8d 34 1c             lea    esi,[r12+rbx*1]
  4004ed:       41 8d 7c 1d 00          lea    edi,[r13+rbx*1+0x0]
  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>
  4004fd:       89 d8                   mov    eax,ebx
  4004ff:       5b                      pop    rbx
  400500:       5d                      pop    rbp
  400501:       41 5c                   pop    r12
  400503:       41 5d                   pop    r13
  400505:       c3                      ret

Montaż z gcc-4.7.3 -Os(wykonuje się w 0,222 sekundy):

00000000004004fa <_ZL3addRKiS0_.isra.0>:
  4004fa:       8d 04 37                lea    eax,[rdi+rsi*1]
  4004fd:       c3                      ret

00000000004004fe <_ZL4workii>:
  4004fe:       41 55                   push   r13
  400500:       41 89 f5                mov    r13d,esi
  400503:       41 54                   push   r12
  400505:       41 89 fc                mov    r12d,edi
  400508:       55                      push   rbp
  400509:       bd 00 c2 eb 0b          mov    ebp,0xbebc200
  40050e:       53                      push   rbx
  40050f:       31 db                   xor    ebx,ebx
  400511:       41 8d 74 1d 00          lea    esi,[r13+rbx*1+0x0]
  400516:       41 8d 3c 1c             lea    edi,[r12+rbx*1]
  40051a:       e8 db ff ff ff          call   4004fa <_ZL3addRKiS0_.isra.0>
  40051f:       01 c3                   add    ebx,eax
  400521:       ff cd                   dec    ebp
  400523:       75 ec                   jne    400511 <_ZL4workii+0x13>
  400525:       89 d8                   mov    eax,ebx
  400527:       5b                      pop    rbx
  400528:       5d                      pop    rbp
  400529:       41 5c                   pop    r12
  40052b:       41 5d                   pop    r13
  40052d:       c3                      ret

Montaż z gcc-4.8.1 -Os(wykonuje się w 0,994 s):

00000000004004fd <_ZL3addRKiS0_.isra.0>:
  4004fd:       8d 04 37                lea    eax,[rdi+rsi*1]
  400500:       c3                      ret

0000000000400501 <_ZL4workii>:
  400501:       41 55                   push   r13
  400503:       41 89 f5                mov    r13d,esi
  400506:       41 54                   push   r12
  400508:       41 89 fc                mov    r12d,edi
  40050b:       55                      push   rbp
  40050c:       bd 00 c2 eb 0b          mov    ebp,0xbebc200
  400511:       53                      push   rbx
  400512:       31 db                   xor    ebx,ebx
  400514:       41 8d 74 1d 00          lea    esi,[r13+rbx*1+0x0]
  400519:       41 8d 3c 1c             lea    edi,[r12+rbx*1]
  40051d:       e8 db ff ff ff          call   4004fd <_ZL3addRKiS0_.isra.0>
  400522:       01 c3                   add    ebx,eax
  400524:       ff cd                   dec    ebp
  400526:       75 ec                   jne    400514 <_ZL4workii+0x13>
  400528:       89 d8                   mov    eax,ebx
  40052a:       5b                      pop    rbx
  40052b:       5d                      pop    rbp
  40052c:       41 5c                   pop    r12
  40052e:       41 5d                   pop    r13
  400530:       c3                      ret

186
Żeby było jasne: czy rzeczywiście zmierzyłeś wydajność kodu OP na 12 różnych platformach? (+1 za samą myśl, że to zrobisz)
anatolyg

194
@anatolyg Tak, zrobiłem to! (i doda kilka wkrótce)
Marat Dukhan

43
W rzeczy samej. Kolejny +1 za nie tylko teoretykę różnych procesorów, ale także udowodnienie tego. Nie coś (niestety) widać w każdej odpowiedzi dotyczącej prędkości. Czy te testy są uruchamiane z tym samym systemem operacyjnym? (Jak to możliwe, wypacza to wynik ...)
usr2564301,

7
@Ali Na AMD-FX 6300 -O2 -fno-align-functions -fno-align-loopsupuszcza czas 0.340s, więc można to wyjaśnić przez wyrównanie. Jednak optymalne wyrównanie zależy od procesora: niektóre procesory preferują wyrównane pętle i funkcje.
Marat Dukhan,

13
@Jwareware Nie widzę, jak system operacyjny miałby znaczący wpływ na wyniki; pętla nigdy nie wykonuje wywołań systemowych.
Ali,

186

Mój kolega pomógł mi znaleźć wiarygodną odpowiedź na moje pytanie. Zauważył wagę granicy 256 bajtów. Nie jest tu zarejestrowany i zachęcił mnie do opublikowania odpowiedzi osobiście (i weź całą sławę).


Krótka odpowiedź:

Czy winowajcą jest winowajca w tym przypadku? Dlaczego i jak?

Wszystko sprowadza się do wyrównania. Dostosowania mogą mieć znaczący wpływ na wydajność, dlatego -falign-*na pierwszym miejscu są flagi.

Ja składać się (Bogus?) Raport o błędzie do programistów gcc . Okazuje się, że domyślnym zachowaniem jest domyślnie wyrównujemy pętle do 8 bajtów, ale próbujemy wyrównać go do 16 bajtów, jeśli nie musimy wypełniać więcej niż 10 bajtów”. Najwyraźniej ta domyślna opcja nie jest najlepszym wyborem w tym konkretnym przypadku i na moim komputerze. Clang 3.4 (trunk) -O3wykonuje odpowiednie wyrównanie, a wygenerowany kod nie pokazuje tego dziwnego zachowania.

Oczywiście, jeśli zostanie wykonane niewłaściwe wyrównanie, pogorszy to sytuację. Niepotrzebne / złe wyrównanie po prostu zjada bajty bez powodu i potencjalnie zwiększa liczbę braków w pamięci podręcznej itp.

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 kodach źródłowych C lub C ++?

Po prostu mówiąc gcc, aby wykonał właściwe wyrównanie:

g++ -O2 -falign-functions=16 -falign-loops=16


Długa odpowiedź:

Kod będzie działał wolniej, jeśli:

  • an XXbajt brzegowe cięcia add()w środku ( XXbędąc uzależnionym od maszyny).

  • jeśli wezwanie do add()musi przeskoczyć XXgranicę bajtu, a cel nie jest wyrównany.

  • jeśli add()nie jest wyrównany.

  • jeśli pętla nie jest wyrównana.

Pierwsze 2 są pięknie widoczne na kodach i wynikach, które uprzejmie opublikował Marat Dukhan . W takim przypadku gcc-4.8.1 -Os(wykonuje się w 0,994 s):

00000000004004fd <_ZL3addRKiS0_.isra.0>:
  4004fd:       8d 04 37                lea    eax,[rdi+rsi*1]
  400500:       c3   

256-bajtowa granica przecina się add()na środku i add()ani pętla nie jest wyrównana. Niespodzianka, niespodzianka, to najwolniejszy przypadek!

W przypadku gcc-4.7.3 -Os(wykonuje się w 0,222 s) granica 256 bajtów przecina tylko zimną sekcję (ale ani pętla, ani nie add()jest cięta):

00000000004004fa <_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>

Nic nie jest wyrównane, a wywołanie add()musi przeskoczyć granicę 256 bajtów. Ten kod jest drugim najwolniejszym.

W przypadku gcc-4.6.4 -Os(wykonuje się w 0,709 sek.), Chociaż nic nie jest wyrównane, wywołanie do add()nie musi przeskakiwać granicy 256 bajtów, a cel jest oddalony o dokładnie 32 bajty:

  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>

To najszybszy ze wszystkich trzech. Dlaczego granica 256 bajtów jest specyficzna na jego maszynie, zostawię to mu, aby ją rozgryźć. Nie mam takiego procesora.

Teraz na mojej maszynie nie widzę efektu granicy 256 bajtów. Na mojej maszynie uruchamia się tylko funkcja i wyrównanie pętli. Jeśli zdam, g++ -O2 -falign-functions=16 -falign-loops=16wszystko wraca do normy: zawsze otrzymuję najszybszy przypadek i czas nie jest już wrażliwy na -fno-omit-frame-pointerflagę. Mogę przekazać g++ -O2 -falign-functions=32 -falign-loops=32lub dowolne wielokrotności 16, kod też nie jest wrażliwy na to.

Po raz pierwszy zauważyłem w 2009 r., Że gcc (przynajmniej w moich projektach i na moich maszynach) ma tendencję do generowania zauważalnie szybszego kodu, jeśli optymalizuję rozmiar (-Os) zamiast prędkości (-O2 lub -O3) i zastanawiam się odtąd dlaczego.

Prawdopodobnym wyjaśnieniem jest to, że miałem punkty aktywne wrażliwe na wyrównanie, tak jak w tym przykładzie. Przez bałagan z flagami (przekazywanie -Oszamiast -O2), te punkty aktywne zostały przypadkowo wyrównane, a kod stał się szybszy. Nie miało to nic wspólnego z optymalizacją pod kątem wielkości: przypadkiem przypadkiem punkty dostępowe zostały lepiej dostosowane. Od teraz sprawdzę efekty wyrównania moich projektów.

Aha i jeszcze jedno. Jak mogą powstać takie punkty aktywne, takie jak pokazane w przykładzie? W jaki sposób inliniowanie tak małej funkcji jak add()zawodzenie może zawieść?

Rozważ to:

// add.cpp
int add(const int& x, const int& y) {
    return x + y;
}

oraz w osobnym pliku:

// 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;
}

i skompilowany jako: g++ -O2 add.cpp main.cpp.

      gcc nie będzie inline add()!

To wszystko, tak łatwo nieumyślnie utworzyć punkty aktywne, takie jak ten w PO. Oczywiście to częściowo moja wina: gcc jest doskonałym kompilatorem. Jeśli kompilacji powyższego jako: g++ -O2 -flto add.cpp main.cpp, czyli gdybym wykonać łącza optymalizację czasu, uruchamia kod w 0.19s!

(Inlining jest sztucznie wyłączony w OP, stąd kod w OP był 2x wolniejszy).


19
Wow ... To zdecydowanie wykracza poza to, co zwykle robię, aby obejść anomalie porównawcze.
Mysticial

@Ali Myślę, że to ma sens, ponieważ w jaki sposób kompilator może wstawić coś, czego nie widzi? Prawdopodobnie dlatego używamy inlinedefinicji funkcji + w nagłówku. Nie jestem pewien, jak dojrzałe lto jest w gcc. Moje doświadczenia z tym przynajmniej w mingw są hitem lub chybił.
greatwolf

7
Myślę, że to Komunikacja firmy ACM kilka lat temu napisało artykuł o uruchamianiu dość dużych aplikacji (perl, Spice itp.), Jednocześnie przesuwając cały obraz binarny o jeden bajt za pomocą środowisk Linux o różnych rozmiarach. Pamiętam typową wariancję około 15%. Ich podsumowanie było takie, że wiele wyników testów porównawczych jest bezużytecznych, ponieważ ta zewnętrzna zmienna wyrównania nie jest brana pod uwagę.
Gene

1
szczególnie chciałbym -flto. to całkiem rewolucyjne, jeśli nigdy wcześniej go nie używałeś, mówiąc z doświadczenia :)
underscore_d

2
To fantastyczny film, który mówi o tym, jak wyrównanie może wpłynąć na wydajność i jak profilować: youtube.com/watch?time_continue=1&v=r-TLSBdHe1A
Zhro

73

Dodam to po zaakceptowaniu, aby podkreślić, że zbadano wpływ dostosowania na ogólną wydajność programów - w tym dużych. Na przykład ten artykuł (i uważam, że jego wersja pojawiła się również w CACM) pokazuje, w jaki sposób sama kolejność łączy i zmiany rozmiaru środowiska systemu operacyjnego były wystarczające, aby znacznie zmienić wydajność. Przypisują to do wyrównania „gorących pętli”.

Artykuł zatytułowany „Wytwarzanie niewłaściwych danych bez robienia niczego oczywiście złego!” mówi, że nieumyślne stronniczość eksperymentalna z powodu prawie niekontrolowanych różnic w środowiskach uruchomionych programów prawdopodobnie unieważnia wiele wyników testów.

Myślę, że napotykasz inny kąt widzenia podczas tej samej obserwacji.

W przypadku kodu krytycznego dla wydajności jest to całkiem dobry argument dla systemów, które oceniają środowisko podczas instalacji lub uruchamiania i wybierają najlepsze lokalne spośród różnych zoptymalizowanych wersji kluczowych procedur.


33

Myślę, że możesz uzyskać taki sam wynik, jak to, co zrobiłeś:

Złapałem zestaw dla opcji -O2 i połączyłem wszystkie jego różnice w zestawie dla opcji -O2, z wyjątkiem linii .p2align:

… Za pomocą -O2 -falign-functions=1 -falign-jumps=1 -falign-loops=1 -falign-labels=1. Kompiluję wszystko z tymi opcjami, które były szybsze niż zwykłe -O2za każdym razem, gdy próbowałem zmierzyć, przez 15 lat.

Ponadto, dla zupełnie innego kontekstu (w tym innego kompilatora), zauważyłem, że sytuacja jest podobna : opcja, która ma „optymalizować rozmiar kodu zamiast prędkości” optymalizuje rozmiar i szybkość kodu.

Jeśli dobrze się domyślam, są to podkładki do wyrównania stosu.

Nie, to nie ma nic wspólnego ze stosem, domyślnie generowane są NOP, a opcje -falign - * = 1 zapobiegają wyrównaniu kodu.

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?

Jest bardzo prawdopodobne, że przyczyną jest wyściółka. Powód jest uważany za konieczny i przydatny w niektórych przypadkach, ponieważ kod jest zazwyczaj pobierany w wierszach o wielkości 16 bajtów (szczegółowe informacje można znaleźć w zasobach optymalizacyjnych Agner Fog , które różnią się w zależności od modelu procesora). Wyrównanie funkcji, pętli lub etykiety na 16-bajtowej granicy oznacza, że ​​szanse są statystycznie zwiększone, że do zawarcia funkcji lub pętli będzie potrzebna jedna mniejsza liczba wierszy. Oczywiście odpala, ponieważ te NOP zmniejszają gęstość kodu, a tym samym wydajność pamięci podręcznej. W przypadku pętli i etykiety NOP mogą nawet wymagać wykonania raz (gdy wykonanie dotrze do pętli / etykiety normalnie, w przeciwieństwie do skoku).


Zabawne jest to: -O2 -fno-omit-frame-pointerjest tak samo dobry jak -Os. Sprawdź zaktualizowane pytanie.
Ali,

11

Jeśli twój program jest ograniczony pamięcią podręczną CODE L1, optymalizacja rozmiaru nagle zaczyna się opłacać.

Kiedy ostatnio sprawdzałem, kompilator nie jest wystarczająco inteligentny, aby to rozgryźć we wszystkich przypadkach.

W twoim przypadku -O3 prawdopodobnie generuje kod wystarczający dla dwóch linii pamięci podręcznej, ale -Os mieści się w jednej linii pamięci podręcznej.


1
Ile chcesz postawić te parametry wyrównania = odnoszą się do wielkości linii pamięci podręcznej?
Joshua

Już mnie to nie obchodzi: nie widać tego na moim komputerze. A po przekazaniu -falign-*=16flag wszystko wraca do normy, wszystko zachowuje się konsekwentnie. Jeśli o mnie chodzi, to pytanie zostało rozwiązane.
Ali

7

W żadnym wypadku nie jestem ekspertem w tej dziedzinie, ale wydaje mi się, że nowoczesne procesory są dość wrażliwe, jeśli chodzi o przewidywanie gałęzi . Algorytmy stosowane do przewidywania rozgałęzień są (a przynajmniej były w czasach, gdy pisałem kod asemblera) oparte na kilku właściwościach kodu, w tym odległości od celu i kierunku.

Scenariusz, który przychodzi mi na myśl, to małe pętle. Gdy gałąź cofała się, a odległość nie była zbyt duża, przewidywanie gałęzi optymalizowało się w tym przypadku, ponieważ wszystkie małe pętle są wykonywane w ten sposób. Te same zasady mogą wejść w grę, gdy zamienisz lokalizację addi workw wygenerowanym kodzie lub gdy pozycja obu nieznacznie się zmieni.

To powiedziawszy, nie mam pojęcia, jak to zweryfikować, i chciałem tylko poinformować cię, że może to być coś, na co chcesz spojrzeć.


Dzięki. Grałem z tym: przyśpieszam tylko poprzez zamianę add()i work()jeśli -O2zostanie zaliczony. We wszystkich innych przypadkach kod staje się znacznie wolniejszy przez zamianę. Podczas weekendu analizowałem również statystyki przewidywania gałęzi / błędnych prognoz perfi nie zauważyłem niczego, co mogłoby wyjaśnić to dziwne zachowanie. Jedynym spójnym wynikiem jest to, że w wolnym przypadku perfzgłasza 100,0 cali add()i dużą wartość na linii zaraz po wywołaniu add()w pętli. Wygląda na to, że z jakiegoś powodu przeciągamy się add()w wolnym przypadku, ale nie w szybkich biegach.
Ali,

Zastanawiam się nad zainstalowaniem VTune Intela na jednym z moich komputerów i samodzielnym profilowaniem. perfobsługuje tylko ograniczoną liczbę rzeczy, być może rzeczy Intela są nieco bardziej przydatne na ich własnym procesorze.
Ali
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.