Компиляторы создают лучший код для циклов do-while по сравнению с другими типами циклов?



есть комментарий библиотека сжатия zlib (который используется в проекте Chromium среди многих других), что означает, что цикл do-while в 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



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



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

537   6  

6 ответов:

прежде всего:

A do-while цикл не то же самое, что while-петля или for-петли.

  • while и for петли могут вообще не запускать тело цикла.
  • A do-while loop всегда запускает тело цикла по крайней мере один раз - он пропускает проверку начального состояния.

так вот в чем логическая разница. Тем не менее, не все строго придерживаются этого. Это довольно распространено для while или for петли, которые будут использоваться даже когда гарантируется, что он всегда будет петлей хотя бы один раз. (Особенно в языках с foreach петли.)

for петли снова, так как они по существу while петли с небольшим количеством синтаксического сахара для счетчика циклов.

поэтому я отвечу на вопрос:

если a while цикл гарантировано цикла по крайней мере один раз, есть ли прирост производительности от использования do-while вместо петли.


A do-while пропускает первую проверку состояния. Таким образом, есть еще одна ветвь и одно условие для оценки.

если условие дорого проверить, и вы знаете, что вы гарантированно цикл по крайней мере один раз, то a do-while цикл может быть быстрее.

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


другими словами, a while-loop:

while (condition){
    body
}

фактически то же самое, что и это:

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

если вы знаете, что вы всегда будете цикл по крайней мере один раз, что if-оператор является посторонним.


аналогично на уровне сборки, это примерно так, как компилируются различные циклы к:

do-while loop:

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 удается избежать дублирования кода условия, но все же генерирует дополнительный условный прыжок.

так что это иронично, что в то время как пример в вопросе подчеркивает, почему a do-while цикл может быть быстрее в общем случае, сам пример, похоже, не дает никаких преимуществ на современном компиляторе.

учитывая, сколько лет было комментарию, мы можем только догадываюсь, почему это имеет значение. Очень возможно, что компиляторы в то время не были способны распознать, что тело было пустым. (А если и так, то они не использовали эту информацию.)

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

Не много, если вы не посмотрите на фактический сгенерированная сборка фактический, конкретный компилятор на конкретной платформы С специальные настройки оптимизации.

Это, вероятно, стоило беспокоиться о десятилетиях назад (когда ZLib был написан), но, конечно, не в наши дни, если вы не нашли, by реальные профилирования, что это устраняет узкое место в коде.

в двух словах (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    x1,%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    x1,%rax
  400434:       0f b6 48 ff             movzbl 0xffffffffffffffff(%rax),%ecx
  400438:       48 83 c2 01             add    x1,%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    x5,%rax
  400531:       75 ed                   jne    400520 <main+0x20>

что просто показывает, что компиляторы совершенно разные и продвигаются гораздо быстрее, чем некоторые программисты могли ожидать несколько лет назад. Это также означает, что этот комментарий довольно бессмыслен и, вероятно, есть, потому что никто никогда не проверял, имеет ли он смысл.


итог-если вы хотите оптимизировать до наилучшего кода (и вы знаете, как это должно выглядеть), сделайте это непосредственно в сборке и вырежьте "средний человек" (компилятор) из уравнения, но учтите, что новые компиляторы и новые HW могут сделать эту оптимизацию устаревшей. В большинстве случаев гораздо лучше просто позволить компилятору выполнить этот уровень работы для вас и сосредоточиться на оптимизации большого материала.

еще один момент, который должен быть сделан - количество команд (предполагая, что это то, что исходный код OPs был после), ни в коем случае не является хорошим измерением эффективности кода. Не все инструкции были созданы равными, и некоторые из них (например, простые движения reg-to-reg) действительно дешевы, поскольку они оптимизируются процессором. Другая оптимизация может фактически повредить внутреннюю оптимизацию процессора, поэтому в конечном итоге учитывается только правильный бенчмаркинг.

A while цикл часто компилируется как do-while цикл с начальной ветвью к условию, т. е.

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

тогда как компиляция a do-while цикл такой же без начальной ветви. Вы можете видеть из этого, что он по своей сути менее эффективен по стоимости начальной ветви, которая, однако, оплачивается только один раз. [Сравните с наивным способом реализации while, который требует как условной ветви, так и безусловной ветви на итерация.]

сказав это, они не являются действительно сопоставимыми альтернативами. Это является болезненным, чтобы превратить while петля в do-while петли и наоборот. они делают разные вещи. И в этом случае несколько вызовов метода будут полностью доминировать над тем, что компилятор сделал с while в отношении do-while.

замечание не о выборе управляющего оператора (у и А), речь идет о развертывание цикла !!!

Как вы можете видеть, это функция сравнения строк (строковые элементы, вероятно, длиной 2 байта), которая могла быть написана с одним сравнением, а не с четырьмя в ярлыке-и выражении.

эта последняя реализация наверняка быстрее, так как она выполняет одну проверку условия конца строки после каждого сравнения четырех элементов, в то время как стандартное кодирование будет включать одну проверку на сравнение. Иначе говоря, 5 тестов на 4 элемента против 8 тестов на 4 элемента.

в любом случае, он будет работать только в том случае, если длина строки кратна четырем или имеет элемент sentinel (так что две строки гарантированно отличаются от strend граница). Довольно рискованно !

это обсуждение, хотя и не эффективность-это совершенно бессмысленно в этом случае, так как нет тела.

while (Condition)
{
}

и

do
{
}
while (Condition);

абсолютно эквивалентны.

Comments

    Ничего не найдено.