Чи створюють компілятори кращий код для циклів виконуваної роботи порівняно з циклами інших типів?


89

У бібліотеці стиснення zlib (яка серед багатьох інших використовується в проекті Chromium) є коментар, який передбачає, що цикл виконуваної роботи в C генерує "кращий" код для більшості компіляторів. Ось фрагмент коду, де він з’являється.

do {
} while (*(ushf*)(scan+=2) == *(ushf*)(match+=2) &&
         *(ushf*)(scan+=2) == *(ushf*)(match+=2) &&
         *(ushf*)(scan+=2) == *(ushf*)(match+=2) &&
         *(ushf*)(scan+=2) == *(ushf*)(match+=2) &&
         scan < strend);
/* The funny "do {}" generates better code on most compilers */

https://code.google.com/p/chromium/codesearch#chromium/src/third_party/zlib/deflate.c&l=1225

Чи є докази того, що більшість (або будь-які) компілятори генерують кращий (наприклад, більш ефективний) код?

Оновлення: Марк Адлер , один з оригінальних авторів, дав трохи контексту в коментарях.


7
До речі, для уточнення, це не є частиною Хрому. Як випливає з URL-адреси, це „сторонній” проект, і якщо поглянути на нього ще уважніше, можна зрозуміти, що цей код походить від ZLib, широко використовуваної бібліотеки стиснення загального призначення.

1
The funny "do {}" generates better code--- краще ніж що? Чим смішно в той час () чи чим нудно, регулярно {}?
п. 'займенники' m.

@ H2CO3 дякую за роз'яснення, я відредагував питання, щоб бути більш конкретним щодо походження.
Денніс

42
Цей коментар був написаний більше 18 років тому в епоху компіляторів Borland та Sun C. Будь-яке значення для компіляторів сьогодні було б абсолютно випадковим. Зверніть увагу, що саме це використання do, на відміну від просто a while, не уникає умовної гілки.
Марк Адлер,

Відповіді:


108

Поперше:

do-whileПетля не те ж саме , як while-loop або for-loop.

  • while і for цикли можуть взагалі не запускати тіло циклу.
  • A do-whileЦикл завжди виконується тіло циклу принаймні один раз - він пропускає першу перевірку умови.

Тож це логічна різниця. Тим не менш, не всі суворо цього дотримуються. Це досить часто для whileабоfor циклів використовується навіть тоді, коли гарантується, що він завжди буде цикл хоча б один раз. (Особливо в мовах із циклами foreach .)

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

Тож я відповім на запитання:

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


A do-whileПропускає першу перевірку умови. Отже, є одна гілка менше і одна менша умова для оцінки.

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

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


Іншими словами, цикл while:

while (condition){
    body
}

Фактично такий самий, як цей:

if (condition){
    do{
        body
    }while (condition);
}

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


Так само на рівні збірки, приблизно так компілюються різні цикли, щоб:

цикл do-while:

start:
    body
    test
    conditional jump to start

while-loop:

    test
    conditional jump to end
start:
    body
    test
    conditional jump to start
end:

Зверніть увагу, що умова продубльовано. Альтернативний підхід:

    unconditional jump to end
start:
    body
end:
    test
    conditional jump to start

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

У будь-якому випадку, це все одно гірше звичайного do-whileциклу.

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


Але для конкретного прикладу у питанні все трохи дивно, оскільки він має порожнє тіло циклу. Оскільки немає тіла, немає логічної різниці між whileі do-while.

FWIW, я тестував це у Visual Studio 2012:

  • З порожнім тілом він насправді генерує той самий код для whileі do-while. Тож ця частина, швидше за все, є залишком старих часів, коли компілятори не були такими чудовими.

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

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

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


12
Тож перевірка стану ще раз настільки велика перевага? Я дуже сумніваюся в цьому. Запустіть цикл 100 разів, і він стає зовсім незначним.

7
@ H2CO3 Але що, якщо цикл працює лише один-два рази? А що щодо збільшеного розміру коду із продубльованого коду умови?
Містичний

6
@Mystical Якщо цикл працює лише один-два рази, тоді цей цикл не варто оптимізувати. І збільшений розмір коду - це, в кращому випадку, не вагомий аргумент. Не обов’язково, щоб кожен компілятор реалізовував його так, як ви показали. Я написав компілятор для власної мови іграшок, і компіляція циклів while реалізована з безумовним переходом до початку циклу, тому код умови видається лише один раз.

30
@ H2CO3 "Якщо цикл працює лише один-два рази, тоді цей цикл не варто оптимізувати." - Дозволю собі не погодитися. Це може бути всередині іншого циклу. Тонни мого власного високооптимізованого коду HPC виглядають так. І так, функція виконування часу має значення.
Містичний

29
@ H2CO3 Де я сказав, що заохочую це? Питання задає do-whileцикл швидший, ніж цикл while. І я відповів на запитання, сказавши, що це може бути швидше. Я не сказав на скільки. Я не сказав, чи варто це робити. Я не рекомендував нікому починати перетворення на цикли do-while. Але просто заперечення того, що існує можливість оптимізації, навіть якщо вона невелика, на мій погляд, є поганою послугою для тих, хто піклується і цікавиться цими речами.
Містичний

24

Чи є докази того, що більшість (або будь-які) компілятори генерують кращий (наприклад, більш ефективний) код?

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

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


9
Ну що ж - фраза тут premature optimizationспадає на думку.
James Snell

@JamesSnell точно. І це те, що найкраща відповідь підтримує / заохочує.

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

16

У двох словах (tl; dr):

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


Подробиці:

Важко сказати, що мав на увазі оригінальний автор під своїм коментарем щодо цього do {} whileкращого коду, але я хотів би міркувати в іншому напрямку, ніж те, що було порушено тут - ми вважаємо, що різниця між циклами do {} whileта while {}циклами досить невелика (одна гілка менше, ніж Містичний сказав), але в цьому коді є щось ще "смішніше", і це вкладає всю роботу всередину цього шаленого стану, а внутрішня частина залишається порожньою ( do {}).

Я спробував наступний код на gcc 4.8.1 (-O3), і це дає цікаву різницю -

#include "stdio.h" 
int main (){
    char buf[10];
    char *str = "hello";
    char *src = str, *dst = buf;

    char res;
    do {                            // loop 1
        res = (*dst++ = *src++);
    } while (res);
    printf ("%s\n", buf);

    src = str;
    dst = buf;
    do {                            // loop 2
    } while (*dst++ = *src++);
    printf ("%s\n", buf);

    return 0; 
}

Після складання -

00000000004003f0 <main>:
  ... 
; loop 1  
  400400:       48 89 ce                mov    %rcx,%rsi
  400403:       48 83 c0 01             add    $0x1,%rax
  400407:       0f b6 50 ff             movzbl 0xffffffffffffffff(%rax),%edx
  40040b:       48 8d 4e 01             lea    0x1(%rsi),%rcx
  40040f:       84 d2                   test   %dl,%dl
  400411:       88 16                   mov    %dl,(%rsi)
  400413:       75 eb                   jne    400400 <main+0x10>
  ...
;loop 2
  400430:       48 83 c0 01             add    $0x1,%rax
  400434:       0f b6 48 ff             movzbl 0xffffffffffffffff(%rax),%ecx
  400438:       48 83 c2 01             add    $0x1,%rdx
  40043c:       84 c9                   test   %cl,%cl
  40043e:       88 4a ff                mov    %cl,0xffffffffffffffff(%rdx)
  400441:       75 ed                   jne    400430 <main+0x40>
  ...

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


З іншого боку, на clang 3.3 (-O3) обидві петлі генерують цей 5 інструкцій:

  400520:       8a 88 a0 06 40 00       mov    0x4006a0(%rax),%cl
  400526:       88 4c 04 10             mov    %cl,0x10(%rsp,%rax,1)
  40052a:       48 ff c0                inc    %rax
  40052d:       48 83 f8 05             cmp    $0x5,%rax
  400531:       75 ed                   jne    400520 <main+0x20>

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


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

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


Схоже, це зберігає переміщення реєстру. mov %rcx,%rsi:) Я бачу, як перестановка коду може це зробити.
Містичний

@Mystical, ти маєш рацію щодо мікрооптимізації. Іноді навіть збереження однієї інструкції нічого не варте (і переходи з рег-в-рег сьогодні повинні бути майже безкоштовними з перейменуванням рег).
Leeor

Здається, перейменування ходів не було здійснено до AMD Bulldozer та Intel Ivy Bridge. Це сюрприз!
Містичний

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

3
Схоже, ви інтерпретували коментар в оригінальному коді інакше, ніж більшість, і це має сенс. У коментарі сказано "смішне робить {} ..", але не сказано, з якою нежартовною версією він порівнюється. Більшість людей знають різницю між виконанням і часом, тому, я здогадуюсь, "смішне робити {}" стосувалося не цього, а розгортання циклу та / або відсутності додаткового призначення, як ви показали тут.
Абель

10

whileЦикл часто скомпільовано як do-whileпетля з початкової гілкою до умові, тобто

    bra $1    ; unconditional branch to the condition
$2:
    ; loop body
$1:
    tst <condition> ; the condition
    brt $2    ; branch if condition true

тоді як компіляція do-whileциклу однакова без початкової гілки. З цього видно, що while()за своєю суттю менш ефективно за вартістю початкової гілки, яка, однак, сплачується лише один раз. [Порівняйте з наївним способом реалізації, while,який вимагає як умовної гілки, так і безумовної гілки на ітерацію.]

Сказавши це, вони насправді не є порівнянними альтернативами. Болісно перетворювати whileцикл на do-whileцикл і навпаки. Вони роблять різні речі. І в цьому випадку кілька викликів методів буде повністю домінувати все , що компілятор зробив з whileпротиdo-while.


7

Зауваження стосується не вибору оператора управління (do vs. while), а розгортання циклу !!!

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

Ця остання реалізація напевно швидша, оскільки виконує одну перевірку умови кінця рядка після кожних чотирьох порівнянь елементів, тоді як стандартне кодування передбачає одну перевірку на порівняння. Сказано інакше, 5 тестів на 4 елементи проти 8 тестів на 4 елементи.

У будь-якому випадку, це буде працювати, лише якщо довжина рядка кратна чотирьом або має сторожовий елемент (так що обидва рядки гарантовано відрізнятимуться за strendмежею). Досить ризиковано!


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

@Dennis: різні компілятори мають різні способи оптимізації згенерованого коду. Деякі можуть самостійно розгортати цикл (до певної міри) або оптимізувати віддалені призначення. Тут кодер змушує компілятор до розгортання циклу, роблячи менш оптимізуючі компілятори ще непоганими. Я думаю, що Ів абсолютно правий щодо своїх припущень, але без оригінального кодера навколо залишається загадкою, яка справжня думка стояла за цим «кумедним» зауваженням.
Абель

1
@Abel дякую за роз'яснення, я зараз краще розумію (передбачуване) значення коментаря. Ів, безумовно, найближче підійшов до розгадки таємниці, яка стоїть за коментарем, але я збираюся прийняти відповідь Містіціала, оскільки, на мою думку, він найкраще відповів на моє запитання. Виявляється, я задавав неправильне запитання, тому що коментар вводить мене в оману зосередитись на типі циклу, хоча це, мабуть, стосується умови.
Денніс

0

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

while (Condition)
{
}

і

do
{
}
while (Condition);

абсолютно рівнозначні.

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