Чому оптимізований простий цикл, коли межа становить 959, але не 960?


131

Розглянемо цю просту петлю:

float f(float x[]) {
  float p = 1.0;
  for (int i = 0; i < 959; i++)
    p += 1;
  return p;
}

Якщо ви компілюєте з gcc 7 (знімок) або clang (trunk), -march=core-avx2 -Ofastви отримаєте щось дуже схоже на.

.LCPI0_0:
        .long   1148190720              # float 960
f:                                      # @f
        vmovss  xmm0, dword ptr [rip + .LCPI0_0] # xmm0 = mem[0],zero,zero,zero
        ret

Іншими словами, він просто встановлює відповідь на 960 без циклу.

Однак якщо ви змінили код на:

float f(float x[]) {
  float p = 1.0;
  for (int i = 0; i < 960; i++)
    p += 1;
  return p;
}

Вироблена збірка фактично виконує суму циклу? Наприклад, clang дає:

.LCPI0_0:
        .long   1065353216              # float 1
.LCPI0_1:
        .long   1086324736              # float 6
f:                                      # @f
        vmovss  xmm0, dword ptr [rip + .LCPI0_0] # xmm0 = mem[0],zero,zero,zero
        vxorps  ymm1, ymm1, ymm1
        mov     eax, 960
        vbroadcastss    ymm2, dword ptr [rip + .LCPI0_1]
        vxorps  ymm3, ymm3, ymm3
        vxorps  ymm4, ymm4, ymm4
.LBB0_1:                                # =>This Inner Loop Header: Depth=1
        vaddps  ymm0, ymm0, ymm2
        vaddps  ymm1, ymm1, ymm2
        vaddps  ymm3, ymm3, ymm2
        vaddps  ymm4, ymm4, ymm2
        add     eax, -192
        jne     .LBB0_1
        vaddps  ymm0, ymm1, ymm0
        vaddps  ymm0, ymm3, ymm0
        vaddps  ymm0, ymm4, ymm0
        vextractf128    xmm1, ymm0, 1
        vaddps  ymm0, ymm0, ymm1
        vpermilpd       xmm1, xmm0, 1   # xmm1 = xmm0[1,0]
        vaddps  ymm0, ymm0, ymm1
        vhaddps ymm0, ymm0, ymm0
        vzeroupper
        ret

Чому це так і чому це точно так само для clang та gcc?


Обмеження для тієї ж петлі, якщо ви заміните floatна double479. Це те саме для gcc та clang знову.

Оновлення 1

Виявляється, що gcc 7 (знімок) і clang (trunk) ведуть себе дуже по-різному. Наскільки я можу сказати, "clang" оптимізує цикли на всі межі менше 960. gcc, з іншого боку, чутливий до точного значення і не має верхньої межі. Наприклад, він не оптимізує цикл, коли межа становить 200 (як і багато інших значень), але він робить, коли межа становить 202 та 20002 (як і багато інших значень).


3
Що, напевно, означає Султан, це те, що 1) компілятор розгортає цикл і 2) після його розгортання бачить, що операції з сумою можуть бути згруповані в одну. Якщо цикл не розкручений, операції неможливо згрупувати.
Жан-Франсуа Фабре

3
Якщо непарна кількість петель ускладнює розгортання, останні кілька повторень потрібно робити спеціально. Цього може бути достатньо для переходу оптимізатора в режим, коли він більше не може розпізнати ярлик. Цілком ймовірно, спочатку потрібно додати код для спеціального випадку, а потім доведеться його знову видалити. Використовувати оптимізатор між вухами завжди найкраще :)
Hans Passant

3
@HansPassant Він також оптимізований для будь-якої кількості, меншої ніж 959.
eleanora

6
Чи зазвичай це не робиться з усуненням індукційних змінних, замість того, щоб розгортати шалену кількість? Розгортання з коефіцієнтом 959 божевільне.
Гарольд

4
@eleanora Я грав з цим провідником compilre, і, здається, виконується наступне (якщо говорити лише про знімок gcc): Якщо кількість циклів кратна 4 і принаймні 72, цикл не розкручується (точніше, розкручується коефіцієнт 4); в іншому випадку весь цикл замінюється константою - навіть якщо кількість циклу становить 2000000001. Моя підозра: передчасна оптимізація (як у, передчасне "ей, кратне 4, це добре для розгортання", що блокує подальшу оптимізацію проти a більш ретельно "Яка справа з цією петлею все одно?")
Хаген фон Ейтцен

Відповіді:


88

TL; DR

За замовчуванням поточний знімок GCC 7 веде себе непослідовно, тоді як для попередніх версій встановлено обмеження за замовчуванням PARAM_MAX_COMPLETELY_PEEL_TIMES, яке становить 16. Це може бути відмінено з командного рядка.

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

Версія GCC <= 6.3.0

Відповідним варіантом оптимізації для GCC є -fpeel-loops, який увімкнено опосередковано разом із прапором -Ofast(акцент - мій):

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

Увімкнено за допомогою -O3та / або -fprofile-use.

Більш детальну інформацію можна отримати, додавши -fdump-tree-cunroll:

$ head test.c.151t.cunroll 

;; Function f (f, funcdef_no=0, decl_uid=1919, cgraph_uid=0, symbol_order=0)

Not peeling: upper bound is known so can unroll completely

Повідомлення від /gcc/tree-ssa-loop-ivcanon.c:

if (maxiter >= 0 && maxiter <= npeel)
    {
      if (dump_file)
        fprintf (dump_file, "Not peeling: upper bound is known so can "
         "unroll completely\n");
      return false;
    }

отже, try_peel_loopфункція повертається false.

Більш докладний вихід можна досягти за допомогою -fdump-tree-cunroll-details:

Loop 1 iterates 959 times.
Loop 1 iterates at most 959 times.
Not unrolling loop 1 (--param max-completely-peeled-times limit reached).
Not peeling: upper bound is known so can unroll completely

Можна налаштувати межі, граючи з max-completely-peeled-insns=nі max-completely-peel-times=nпарами:

max-completely-peeled-insns

Максимальна кількість розсилок повністю очищеної петлі.

max-completely-peel-times

Максимальна кількість ітерацій петлі, яка підходить для повного відшаровування.

Щоб дізнатися більше про відвідувачі, ви можете ознайомитись з Посібником з внутрішніх справ GCC .

Наприклад, якщо ви компілюєте такі варіанти:

-march=core-avx2 -Ofast --param max-completely-peeled-insns=1000 --param max-completely-peel-times=1000

тоді код перетворюється на:

f:
        vmovss  xmm0, DWORD PTR .LC0[rip]
        ret
.LC0:
        .long   1148207104

Кланг

Я не впевнений, що насправді робить Кланг і як налаштувати його межі, але, як я зауважив, ви можете змусити його оцінити остаточне значення, позначивши цикл розгортанням прагми , і це повністю видалить:

#pragma unroll
for (int i = 0; i < 960; i++)
    p++;

результати в:

.LCPI0_0:
        .long   1148207104              # float 961
f:                                      # @f
        vmovss  xmm0, dword ptr [rip + .LCPI0_0] # xmm0 = mem[0],zero,zero,zero
        ret

Дякую за цю дуже приємну відповідь. Як зазначали інші, gcc, здається, чутливий до точного граничного розміру. Наприклад, він не усуває цикл для 912 godbolt.org/g/EQJHvT . Що в цьому випадку говорять деталі fdump-tree-crollroll?
елеанора

Насправді навіть у 200 є ця проблема. Це все на знімку gcc 7, який надає godbolt. godbolt.org/g/Vg3SVs Це взагалі не стосується кланг .
елеанора

13
Ви пояснюєте механіку пілінгу, але не те, в чому актуальність 960 або чому взагалі існує навіть межа
ММ

1
@MM: поведінка пілінгу абсолютно відрізняється між GCC 6.3.0 та останнім снафостом. У випадку колишнього, я сильно підозрюю, що жорстко закодований ліміт застосовується за допомогою PARAM_MAX_COMPLETELY_PEEL_TIMESпарами, який визначений у /gcc/params.def:321значенні 16.
Grzegorz Szpetkowski

14
Ви можете згадати, чому GCC навмисно обмежує себе таким чином. Зокрема, якщо ви розкручуєте петлі занадто агресивно, двійковий файл збільшується, і ви менше шансів поміститись у кеш-пам'ять L1. Пропуски кешу потенційно досить дорогі відносно економії кількох умовних стрибків, припускаючи хороший прогноз гілки (який у вас буде, для типового циклу).
Кевін

19

Прочитавши коментар Султана, я думаю, що:

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

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

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

Компілятор міг бачити, що розгортання 1000 операторів становить одне додавання, але описані вище кроки 1 та 2 - це дві окремі оптимізації, тому він не може ризикувати розгортанням, не знаючи, чи можна згрупувати операції (наприклад: виклик функції неможливо згрупувати).

Примітка. Це кутовий випадок: хто використовує цикл, щоб знову додати те саме? У цьому випадку не покладайтеся на можливий розгортання / оптимізацію компілятора; безпосередньо написати належну операцію в одній інструкції.


1
тоді ви можете зосередитись на цій not too highчастині? Я маю на увазі, чому ризик не існує у випадку 100? Я щось здогадався ... в коментарі вище ... це може бути причиною цього?
користувач2736738

Я думаю, що компілятор не знає про неточність з плаваючою комою, що це може викликати. Я думаю, це лише обмеження розміру інструкції. Ви max-unrolled-insnsпоручmax-unrolled-times
Жан-Франсуа Фабре

Ага, це було моєю думкою чи здогадкою ... бажаю отримати більш чітке міркування.
користувач2736738

5
Цікаво , якщо ви змінити floatдо int, GCC компілятор може знизити міцність циклу , незалежно від підрахунку ітерацій, з - за його індукцію змінної оптимізації ( -fivopts). Але вони, схоже, не працюють для floats.
Тавіан Барнс

1
@CortAmmon Right, і я пригадую, що читав деяких людей, які були здивовані та засмучені тим, що GCC використовує MPFR для точного обчислення дуже великих чисел, даючи досить різні результати, ніж еквівалентні операції з плаваючою комою, які б накопичили помилки та точність втрат. Виявляється, що багато людей обчислюють плаваючу точку неправильно.
Zan Lynx

12

Дуже гарне запитання!

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

Ви також можете пограти з компілером Godbolt's Compiler Explorer, щоб порівняти, як різні компілятори та параметри впливають на створений код: gcc 6.2і icc 17все-таки вбудований код для 960, тоді як clang 3.9це не так (за конфігурацією Godbolt за замовчуванням, він фактично припиняє вкладку на 73).


Я відредагував питання, щоб зрозуміти версії gcc і clang, якими я користувався. Дивіться godbolt.org/g/FfwWjL . Я, наприклад, використовую -Ofast.
елеанора
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.