Чому GCC генерує 15-20% швидший код, якщо я оптимізую розмір замість швидкості?


445

Я вперше помітив у 2009 році, що GCC (принаймні, на моїх проектах і на моїх машинах) має тенденцію генерувати помітно швидший код, якщо я оптимізую розмір ( -Os) замість швидкості ( -O2або -O3), і з тих пір мені цікаво чому.

Мені вдалося створити (досить нерозумний) код, який показує цю дивовижну поведінку і достатньо малий, щоб розмістити тут.

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

Якщо я компілюю її -Os, для виконання цієї програми потрібно 0,38 с, а якщо вона компілюється з -O2або 0,44 с -O3. Ці часи отримуються послідовно і практично без шуму (gcc 4.7.2, x86_64 GNU / Linux, Intel Core i5-3320M).

(Оновлення: я перемістив увесь код складання до GitHub : вони зробили пост роздутим і, мабуть, додають дуже мало значення питанням, оскільки fno-align-*прапори мають однаковий ефект.)

Ось створена збірка з -Osта -O2.

На жаль, моє розуміння збірки дуже обмежена, так що я поняття не маю то , що я робив далі , було правильно: я схопив збірку для -O2і об'єднати всі свої відмінності в збірку за -Os винятком тих .p2alignліній, результат тут . Цей код все ще працює в 0,38 секунд, і різниця полягає лише в тому, що .p2align речі.

Якщо я гадаю правильно, це прокладки для вирівнювання стека. Згідно з чому функція GCC прокладки функціонує з NOP це робиться з надією, що код запуститься швидше, але, мабуть, ця оптимізація в моєму випадку призвела до помилок.

Чи винуватцем цієї справи є саме підкладка? Чому і як?

Шум, який він створює, унеможливлює мікрооптимізацію часу.

Як я можу переконатись, що такі випадкові вигідні / нещасні вирівнювання не заважають, коли я роблю мікрооптимізацію (не пов'язану з вирівнюванням стека) на вихідному коді C або C ++?


ОНОВЛЕННЯ:

Після відповіді Паскаля Куока я трохи розібрався з вирівнюванням. Переходячи -O2 -fno-align-functions -fno-align-loopsдо gcc, всі .p2alignвідходять від збірки, і згенерований виконуваний файл працює за 0,38 с. Відповідно до документації gcc :

-Os дозволяє всі -O2 оптимізації [але] -Os відключає такі оптимізаційні прапори:

  -falign-functions  -falign-jumps  -falign-loops
  -falign-labels  -freorder-blocks  -freorder-blocks-and-partition
  -fprefetch-loop-arrays

Отже, це, здається, є (помилковим) питанням вирівнювання.

Я все ще скептично ставлюсь до-march=native пропозицій у відповіді Марата Дюхана . Я не переконаний, що це питання не лише втручається в це (неправильне) питання вирівнювання; це абсолютно не впливає на мою машину. (Тим не менш, я підтримав його відповідь.)


ОНОВЛЕННЯ 2:

Ми можемо вийняти -Osз малюнка. Наступні рази отримуються шляхом компіляції з

  • -O2 -fno-omit-frame-pointer 0,37с

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

  • -S -O2потім ручне переміщення збірки add()після work()0,37 с

  • -O2 0,44с

Мені здається, що відстань add()від сайту дзвінка має велике значення. Я намагався perf, але вихід perf statі perf reportмає для мене дуже мало сенсу. Однак я міг отримати лише один послідовний результат від цього:

-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

Для 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
       ¦    }

Для -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

Схоже, ми затримуємо заклик add()у повільному випадку.

Я вивчив усе, що perf -eможе виплюнути на мою машину; не лише статистика, яка наведена вище.

Для одного і того ж виконуваного файлу stalled-cycles-frontendпоказана лінійна кореляція з часом виконання; Я не помітив нічого іншого, що так чітко співвідноситься. (Порівнювати stalled-cycles-frontendрізні версії для мене не має сенсу.)

Я включив пропуски кеша, оскільки він з'явився як перший коментар. Я вивчив усі пропуски кешу, які можна виміряти на моїй машині perf, а не лише ті, що наведені вище. Пропуски кеш-пам’яті дуже шумні і майже не виявляють кореляції з часом виконання.


36
Сліпа здогадка: чи може це пропустити кеш?

@ H2CO3 Це також була моя перша думка, але вона не була достатньо заохочена залишити коментар, не прочитавши і не розібравшись в глибокому розумінні питання ОП.
πάντα ῥεῖ

2
@ g-makulik Ось чому я попередив, що це "сліпа здогадка" ;-) "TL; DR" зарезервована для поганих питань. : P

3
Просто цікавий момент даних: я вважаю, що -O3 або -Ofast приблизно в 1,5 рази швидший, ніж -O, коли я компілюю це з клаксом на OS X. (Я не намагався відтворити з gcc.)
Роб Нап'єр

2
Це той самий код. Роздивіться детальніше адресу .L3, нерівні цілі галузей дорогі.
Ганс Пасант

Відповіді:


505

За замовчуванням компілятори оптимізують для "середнього" процесора. Оскільки різні процесори віддають перевагу різним послідовностям інструкцій, оптимізація компілятора, включена за допомогою -O2процесора, може принести користь середньому процесору, але знизити продуктивність вашого конкретного процесора (і те саме стосується -Os). Якщо ви спробуєте один і той же приклад на різних процесорах, ви побачите, що на деяких з них виграють, -O2а інші - більш сприятливі для -Osоптимізації.

Ось результати для time ./test 0 0кількох процесорів (повідомляється час користувача):

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

У деяких випадках ви можете зменшити дію несприятливих оптимізацій, попросивши gccоптимізувати конкретний процесор (використовуючи параметри -mtune=nativeабо -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

Оновлення: у Core i3 на базі Ivy Bridge три версії gcc( 4.6.4, 4.7.3і 4.8.1) виробляють бінарні файли зі значно різною продуктивністю, але код складання має лише незначні зміни. Поки що у мене немає пояснення цього факту.

Збірка з gcc-4.6.4 -Os(виконується за 0.709 сек):

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

Збірка з gcc-4.7.3 -Os(виконується за 0,822 сек):

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

Збірка з gcc-4.8.1 -Os(виконується за 0,994 сек):

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
Просто для того, щоб зрозуміти: чи дійсно ви йшли і вимірювали ефективність коду OP на 12 різних платформах? (+1 для простої думки, що ви це зробите)
anatolyg

194
@anatolyg Так, я! (і незабаром додамо ще декілька)
Марат Дюхан

43
Справді. Ще один +1 для того, щоб не лише теоретизувати про різні процесори, але й фактично довести це. Не те, що (на жаль) ви бачите в кожній відповіді, що стосується швидкості. Чи проводяться ці тести на одній ОС? (Як це можливо, це перекосує результат ...)
usr2564301

7
@Ali На AMD-FX 6300 -O2 -fno-align-functions -fno-align-loopsвипадає час 0.340s, щоб це було пояснено вирівнюванням. Однак оптимальне вирівнювання залежить від процесора: деякі процесори віддають перевагу вирівняним циклам та функціям.
Марат Дюхан

13
@Jongware Я не бачу, як ОС суттєво вплине на результати; цикл ніколи не здійснює системні виклики.
Алі

186

Мій колега допоміг мені знайти правдоподібну відповідь на моє запитання. Він помітив важливість межі 256 байт. Він не зареєстрований тут і закликав мене опублікувати відповідь сам (і взяти всю славу).


Коротка відповідь:

Чи винуватцем цієї справи є саме підкладка? Чому і як?

Це все зводиться до вирівнювання. Вирівнювання може мати суттєвий вплив на продуктивність, тому ми маємо-falign-* прапорці в першу чергу.

Я надіслав (фальшивий?) Звіт про помилки розробникам gcc . Виявляється, поведінка за замовчуванням - це "вирівнювання циклів до 8 байт за замовчуванням, але намагаємося вирівняти його до 16 байт, якщо нам не потрібно заповнювати більше 10 байт". Мабуть, цей дефолт - не найкращий вибір у цьому конкретному випадку та на моїй машині. Clang 3.4 (магістраль) з -O3виконує відповідне вирівнювання, і згенерований код не демонструє цієї дивної поведінки.

Звичайно, якщо зроблено невідповідне вирівнювання, це погіршує ситуацію. Непотрібне / неправильне вирівнювання просто з’їдає байти без причини і потенційно збільшує пропуски кешу тощо.

Шум, який він створює, унеможливлює мікрооптимізацію часу.

Як я можу переконатися, що такі випадкові вигідні / нещасні вирівнювання не заважають, коли я роблю мікрооптимізацію (не пов'язану з вирівнюванням стека) на вихідних кодах C або C ++?

Просто кажучи gcc зробити правильне вирівнювання:

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


Довга відповідь:

Код буде працювати повільніше, якщо:

  • Н. XXбайтові граничні розрізи add()в середині ( XXбудучи машіннозавісімий).

  • якщо виклик до add()повинен перейти через XXмежу байта, а ціль не вирівняна.

  • якщо add()не вирівняний.

  • якщо петля не вирівняна.

Перші 2 добре видно на кодах та результатах, які люб’язно опублікував Марат Дюхан . У цьому випадку gcc-4.8.1 -Os(виконується за 0,994 сек):

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

межа 256 байтів прорізається add()прямо посередині, і ні add()петля не вирівняна. Сюрприз, сюрприз, це найповільніший випадок!

У випадку gcc-4.7.3 -Os(виконується за 0,822 сек) межа 256 байт розрізається лише в холодному відрізку (але ні цикл, ні add()обрізається):

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>

Нічого не вирівнюється, і заклик до add()повинен перейти через межу 256 байт. Цей код є другим найповільнішим.

У випадку gcc-4.6.4 -Os(виконується за 0.709 сек), хоча нічого не вирівняно, заклик до add()не повинен переходити межу 256 байт, а ціль становить рівно 32 байти:

  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>

Це найшвидший з усіх трьох. Чому межа в 256 байтах є особливою на його машині, я переставлю її розібратися. У мене немає такого процесора.

Тепер на своїй машині я не отримую цього 256-байтового граничного ефекту. На моїй машині починаються лише функції та вирівнювання циклу. Якщо я пройду, g++ -O2 -falign-functions=16 -falign-loops=16то все повернеться до норми: я завжди отримую найшвидший випадок і час вже не чутливий до -fno-omit-frame-pointerпрапора. Я можу пропустити g++ -O2 -falign-functions=32 -falign-loops=32або будь-які кратні 16, код також не чутливий до цього.

Я вперше помітив у 2009 році, що gcc (принаймні, на моїх проектах та на моїх машинах) має тенденцію генерувати помітно швидший код, якщо я оптимізую розмір (-Os) замість швидкості (-O2 або -O3), і мені було цікаво з тих пір, чому.

Ймовірно, пояснення полягає в тому, що у мене були гарячі точки, які були чутливі до вирівнювання, як і у цьому прикладі. Поплутавшись із прапорами (проходячи -Osзамість них -O2), ці гарячі точки випадково вирівнялися випадково, і код став швидшим. Це не мало нічого спільного з оптимізацією розміру: саме випадково гарячі точки краще вирівнялися.Відтепер я буду перевіряти ефекти вирівнювання на мої проекти.

О, і ще одне. Як можуть виникати такі гарячі точки, як показана на прикладі? Як може add()вийти з ладу така крихітна функція, як провал?

Врахуйте це:

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

і скомпільовано як: g++ -O2 add.cpp main.cpp.

      gcc не буде вбудованим add()!

Ось і все, це так просто ненавмисно створити гарячі точки, як у ОП. Звичайно, я частково я винен: gcc - відмінний компілятор. Якщо скласти вищезгадане як:, g++ -O2 -flto add.cpp main.cppтобто, якщо я виконую оптимізацію часу зв'язку, код працює через 0,19s!

(Вбудовування штучно відключається в ОП, отже, код в ОП був у 2 рази повільнішим).


19
Нічого ... Це, безумовно, виходить за рамки того, що я зазвичай роблю, щоб подолати аномалії тестування.
Містичний

@Ali Я думаю, що це має сенс, оскільки як компілятор може вбудувати щось таке, чого він не бачить? Це, мабуть, тому ми використовуємо inlineвизначення функції у заголовку. Не впевнений, наскільки зрілий lto в gcc. Мій досвід роботи з цим принаймні в Mingw - це хіт або промах.
greatwolf

7
Я думаю, що саме в Communications of ACM була стаття кілька років тому про запуск досить великих додатків (perl, Spice тощо) під час переміщення всього бінарного зображення по одному байту за один раз, використовуючи середовища Linux різного розміру. Я пригадую типову дисперсію 15% або близько того. Їх резюме було те, що багато результатів еталону марні, оскільки ця зовнішня змінна вирівнювання не враховується.
Гена

1
особливо для -flto. це досить революційно, якщо ви ніколи цього не використовували, говорячи з досвіду :)
підкреслюйте

2
Це фантастичне відео, в якому розповідається про те, як вирівнювання може впливати на ефективність та як його
додати

73

Я додаю це після прийняття, щоб зазначити, що ефекти вирівнювання на загальну ефективність програм - у тому числі великих - вивчені. Наприклад, ця стаття (і я вважаю, що версія цього також з’явилася в CACM) показує, наскільки порядок зв’язків і розмір середовища ОС тільки були достатніми, щоб значно змінити продуктивність. Вони пов'язують це з вирівнюванням "гарячих циклів".

Цей документ під назвою "Виробляти неправильні дані, не роблячи нічого очевидно неправильного!" говорить про те, що ненавмисне експериментальне упередження через майже неконтрольовані відмінності в програмних середовищах, ймовірно, робить безліч результатів безглуздих.

Я думаю, що ти зустрічаєш інший кут на одне й те саме спостереження.

Що стосується критичного для продуктивності коду, це досить хороший аргумент для систем, які оцінюють середовище під час встановлення або часу роботи та вибирають найкращі локальні версії ключових процедур, що відрізняються оптимізацією.


33

Я думаю, що ви можете отримати той самий результат, що і ви:

Я схопив збірку за -O2 і об'єднав усі її відмінності в збірку для -Os, крім рядків .p2align:

… За допомогою -O2 -falign-functions=1 -falign-jumps=1 -falign-loops=1 -falign-labels=1. Я збираю все з цими варіантами, які були швидшими, ніж просто, -O2щоразу, коли я намагався виміряти, протягом 15 років.

Також для зовсім іншого контексту (включаючи інший компілятор) я помітив, що ситуація схожа : параметр, який повинен "оптимізувати розмір коду, а не швидкість", оптимізує для розміру та швидкості коду.

Якщо я гадаю правильно, це прокладки для вирівнювання стека.

Ні, це не має нічого спільного зі стеком, NOP, які генеруються за замовчуванням, і параметри -falign - * = 1 запобігають для вирівнювання коду.

Відповідно до того, чому функція GCC прокладки функціонує з NOP? це робиться з надією, що код запуститься швидше, але, мабуть, ця оптимізація в моєму випадку призвела до відмови.

Чи винуватцем цієї справи є саме підкладка? Чому і як?

Дуже ймовірно, що винуватцем є підкладка. Причина заміщення вважається необхідною і корисною в деяких випадках полягає в тому, що код зазвичай вибирається в рядках з 16 байтів (див. Ресурси оптимізації Agner Fog для деталей, що залежать від моделі процесора). Вирівнювання функції, циклу чи мітки на 16-байтовій межі означає, що шанси статистично збільшуються, що для утримання функції або циклу буде потрібно один менший ряд. Очевидно, це справляє зворотну реакцію, оскільки ці NOP знижують щільність коду і, отже, ефективність кешу. Що стосується циклів і міток, то НОП може знадобитися навіть один раз виконати (коли виконання приходить до циклу / мітки зазвичай, на відміну від стрибка).


Найсмішніше: -O2 -fno-omit-frame-pointerтак само добре -Os. Перевірте оновлене запитання.
Алі

11

Якщо ваша програма обмежена кешем CODE L1, то оптимізація для розміру раптово починає окупатися.

Коли я востаннє перевіряв, компілятор недостатньо розумний, щоб зрозуміти це у всіх випадках.

У вашому випадку -O3, ймовірно, генерує код, достатній для двох рядків кеша, але -O вписується в один рядок кешу.


1
Скільки ви хочете зробити ставку, що ті вирівнювання = параметри відносяться до розміру рядків кеша?
Джошуа

Мене вже не хвилює: це не видно на моїй машині. І передаючи -falign-*=16прапори, все повертається до норми, все поводиться послідовно. Що стосується мене, це питання вирішено.
Алі

7

Я аж ніяк не експерт у цій галузі, але, здається, пам’ятаю, що сучасні процесори досить чутливі, коли мова йде про галузеве передбачення . Алгоритми, що використовуються для прогнозування гілок, є (або принаймні повернулися в дні, коли я писав код асемблера), засновані на кількох властивостях коду, включаючи відстань до цілі та напрямок.

Сценарій, який спадає на думку, - це невеликі петлі. Коли гілка рухалася назад, а відстань була не надто далеко, передбачення гілки оптимізувало для цього випадку, оскільки всі невеликі петлі робляться таким чином. Ті самі правила можуть діяти, коли ви поміняєте місце розташування addта workзгенерованого коду або коли положення обох незначно змінюється.

Це означає, що я не маю уявлення, як це підтвердити, і я просто хотів повідомити вам, що це може бути те, що ви хочете переглянути.


Дякую. Я грав з цим: я швидко отримую швидкість, міняючи місцями, add()і work()якщо -O2вона проходить. У всіх інших випадках код стає значно повільнішим шляхом заміни. Протягом тижня я також аналізував статистику прогнозування / неправильного прогнозування, perfі я не помітив нічого, що могло б пояснити цю дивну поведінку. Єдиним послідовним результатом є те, що у повільному випадку perfповідомляється 100.0 in add()та велике значення на лінії прямо після виклику add()в циклі. Схоже, ми з певних причин затримуємось add()у повільному випадку, але не у швидких ходах.
Алі

Я думаю про встановлення VTune Intel на одній із моїх машин і займаюся профілізацією самостійно. perfпідтримує лише обмежену кількість речей, можливо, речі Intel набагато зручніші для власного процесора.
Алі
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.