Компиляторы создают лучший код для циклов 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
есть ли какие-либо доказательства того, что большинство (или любые) компиляторы будут генерировать лучше (например более эффективный) код?
обновление:Марк Адлер, одним из оригинальных авторов, дал немного контекста в комментариях.
6 ответов:
прежде всего:
A
do-whileцикл не то же самое, чтоwhile-петля илиfor-петли.
whileиforпетли могут вообще не запускать тело цикла.- A
do-whileloop всегда запускает тело цикла по крайней мере один раз - он пропускает проверку начального состояния.так вот в чем логическая разница. Тем не менее, не все строго придерживаются этого. Это довольно распространено для
for петли снова, так как они по существуwhileилиforпетли, которые будут использоваться даже когда гарантируется, что он всегда будет петлей хотя бы один раз. (Особенно в языках с foreach петли.)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 startwhile-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