Коли, якщо взагалі, розгортання циклу все ще корисно?


93

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

// Search for elements to swap.
while(myArray[++index1] < pivot) {}
while(pivot < myArray[--index2]) {}

Я спробував розгорнути щось на зразок:

while(true) {
    if(myArray[++index1] < pivot) break;
    if(myArray[++index1] < pivot) break;
    // More unrolling
}


while(true) {
    if(pivot < myArray[--index2]) break;
    if(pivot < myArray[--index2]) break;
    // More unrolling
}

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


1
Чи можу я запитати, чому ви не використовуєте стандартні бібліотечні програми швидкого сортування?
Peter Alexander

14
@Poita: Оскільки у мене є деякі додаткові функції, які мені потрібні для статистичних обчислень, які я роблю, і вони дуже налаштовані на мої випадки використання, і тому менш загальні, але помірно швидші, ніж стандартні бібліотеки. Я використовую мову програмування D, яка має старий дерьмовий оптимізатор, і для великих масивів випадкових плаваючих я все ще перевершую сортування ST ++ GCC на 10-20%.
dsimcha

Відповіді:


122

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

Простий приклад:

for (int i=0; i<n; i++)
{
  sum += data[i];
}

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

З іншого боку, цей код:

for (int i=0; i<n; i+=4)
{
  sum1 += data[i+0];
  sum2 += data[i+1];
  sum3 += data[i+2];
  sum4 += data[i+3];
}
sum = sum1 + sum2 + sum3 + sum4;

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


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

2
Приємна відповідь та повчальний приклад. Хоча я не бачу, як зупинки кеш-промахів можуть вплинути на ефективність для цього конкретного прикладу . Я прийшов пояснити собі різницю в продуктивності між двома фрагментами коду (на моїй машині другий фрагмент коду в 2-3 рази швидший), зазначивши, що перший відключає будь-який паралелізм рівня команд на смугах із плаваючою комою. Другий дозволить суперскалярному ЦП виконувати одночасно до чотирьох додавань з плаваючою точкою.
Тобі Брюлл,

2
Майте на увазі, що результат не буде чисельно ідентичним вихідному циклу при обчисленні суми таким чином.
Барабас

Циклічна залежність - це один цикл , додавання. Ядро OoO буде чудово працювати. Тут розгортання може допомогти SIMD з плаваючою комою, але це не про OoO.
Veedrac

2
@Nils: Не дуже; Основні процесори x86 OoO все ще досить подібні до Core2 / Nehalem / K10. Встигання після пропуску кешу було все ще незначним, приховування затримки FP все ще було основною перевагою. У 2010 році центральні процесори, які могли робити 2 завантаження на такт, були ще рідше (просто AMD, оскільки SnB ще не випускався), тому кілька акумуляторів, безумовно, були менш цінними для цілочисельного коду, ніж зараз (звичайно, це скалярний код, який повинен автоматично векторизувати , так що хто знає , чи будуть компілятори перетворити кілька акумуляторів в векторні елементи або в декількох векторних акумулятори ...)
Пітер Кордес

25

Це не зробить жодної різниці, тому що ви виконуєте однакову кількість порівнянь. Ось кращий приклад. Замість:

for (int i=0; i<200; i++) {
  doStuff();
}

написати:

for (int i=0; i<50; i++) {
  doStuff();
  doStuff();
  doStuff();
  doStuff();
}

Навіть тоді це майже напевно не матиме значення, але зараз ви робите 50 порівнянь замість 200 (уявіть, порівняння є більш складним).

Ручне розгортання циклу загалом є, в основному, артефактом історії. Це ще один із зростаючого списку речей, які хороший компілятор зробить для вас, коли це важливо. Наприклад, більшість людей не турбуються писати x <<= 1або x += xзамість цього x *= 2. Ви просто пишете, x *= 2і компілятор оптимізує його для вас, якнайкраще.

В основному стає все менше потреби вгадувати свій компілятор.


1
@Mike Звичайно, вимкнення оптимізації, якщо це гарна ідея, коли спантеличений, але варто прочитати посилання, яке розмістив Poita_. Укладачі стають болісно добрими в цій справі.
dmckee --- екс-модератор кошеня 02.03.10

16
@Mike "Я цілком здатний вирішувати, коли чи коли не робити цих речей" ... Я сумніваюся, якщо ти не надлюдина.
Mr. Boy

5
@ Джон: Я не знаю, чому ти це кажеш; здається, люди думають, що оптимізація - це щось на зразок чорного мистецтва, як це вміють робити лише компілятори та хороші здогади. Все зводиться до інструкцій та циклів та причин, за якими вони витрачаються. Як я вже багато разів пояснював у SO, легко сказати, як і чому вони витрачаються. Якщо у мене є цикл, який повинен використовувати значний відсоток часу, і він витрачає занадто багато циклів накладних витрат циклу, порівняно із вмістом, я бачу це і розгортаю його. Те саме для підйому коду. Для цього не потрібен геній.
Mike Dunlavey

3
Я впевнений, що це не так складно, але я все ще сумніваюся, що ви можете зробити це так швидко, як це робить компілятор. У чому проблема того, що компілятор все одно робить це за вас? Якщо вам це не подобається, просто вимкніть оптимізацію та згоріть так, ніби це 1990 рік!
Mr. Boy

2
Приріст продуктивності завдяки розгортанню циклу не має нічого спільного з порівняннями, які ви економите. Нічого взагалі.
bobbogo

14

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

Варто було б з’ясувати, скільки оптимізацій для вас робить ваш компілятор.

Мені здалося, що виступ Фелікса фон Лайтнера дуже просвітницький на цю тему. Рекомендую прочитати. Короткий зміст: Сучасні компілятори ДУЖЕ розумні, тому оптимізація рук майже ніколи не є ефективною.


7
Це хороше читання, але єдиною частиною, на яку я думав, що є позначка, було те, де він говорить про спрощення структури даних. Решта була точною, але спирається на гігантське невстановлене припущення - що те, що страчується, має бути. У налаштуваннях, які я роблю, я знаходжу людей, які турбуються про реєстрації та пропуски кешу, коли величезна кількість часу йде на непотрібні гори абстракційного коду.
Mike Dunlavey

3
"Оптимізація рук майже ніколи не є ефективною" → Можливо, це вірно, якщо ви абсолютно новачок у завданні. Інакше просто неправда.
Veedrac

У 2019 році я все ще робив розгортання вручну із значним виграшем від автоматичних спроб компілятора .. так що це не так надійно, щоб дозволити компілятору робити все це. Здається, не все так часто розгортається. Принаймні для c # я не можу говорити від імені всіх мов.
WDUK

2

Наскільки я розумію, сучасні компілятори вже розгортають цикли там, де це доречно - наприклад, gcc, якщо передано прапори оптимізації, в інструкції сказано, що це буде:

Розгорніть цикли, кількість ітерацій яких можна визначити під час компіляції або при вході в цикл.

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


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

2

Розгортання шлейфу, будь то розгортання вручну чи розгортання компілятора, часто може призвести до контрпродуктивності, особливо з новішими процесорами x86 (Core 2, Core i7). Підсумок: порівняйте код із розгортанням циклу та без нього на тих процесорах, на яких ви плануєте розгорнути цей код.


Чому саме на процесорних процесорах x86?
JohnTortugo

7
@JohnTortugo: Сучасні процесори x86 мають певні оптимізації для малих циклів - див., Наприклад, детектор потокового циклу на архітектурах Core і Nehalem - розгортання циклу, щоб він вже не був достатньо малим, щоб поміститися в кеш LSD, що перемагає цю оптимізацію. Див., Наприклад, tomshardware.com/reviews/Intel-i7-nehalem-cpu,2041-3.html
Paul R

1

Спроба, не знаючи, не спосіб це зробити.
Це сортування займає високий відсоток від загального часу?

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

Ось приклад того, як отримати максимальну продуктивність.


1

Розгортання петлі може бути корисним у конкретних випадках. Єдиний виграш - це не пропуск деяких тестів!

Наприклад, це може дозволити скалярну заміну, ефективну вставку попереднього завантаження програмного забезпечення ... Ви були б здивовані, наскільки це може бути корисним (ви можете легко отримати 10% прискорення на більшості циклів навіть з -O3) шляхом агресивного розгортання.

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


0

Розгортання петлі цілком залежить від розміру вашої проблеми. Це цілком залежить від того, чи може ваш алгоритм зменшити розмір на менші групи робіт. Те, що ви зробили вище, не схоже на це. Я не впевнений, що моделювання Монте-Карло можна навіть розгорнути.

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


Я розгортав швидке сортування, яке викликається із внутрішнього циклу мого моделювання, а не з головного циклу моделювання.
dsimcha

0

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

У вашому прикладі ви використовуєте невелику кількість локальних змінних, не перевитрачуючи регістри.

Порівняння (до кінця циклу) також є головним недоліком, якщо порівняння важке (тобто не testінструкція), особливо якщо це залежить від зовнішньої функції.

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.