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

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

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

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

Ответ 1

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

Цикл

A 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-loop:

while (condition){
    body
}

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

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

Если вы знаете, что вы всегда будете цитировать хотя бы один раз, этот if-statement является посторонним.


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

цикл do-while:

start:
    body
    test
    conditional jump to start

тогда-цикла:

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

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

Ответ 2

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

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

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

Ответ 3

В двух словах (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 могут сделать эту оптимизацию устаревшей. В большинстве случаев гораздо лучше просто позволить компилятору выполнить этот уровень работы для вас и сосредоточиться на оптимизации большого материала.

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

Ответ 4

A 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 в цикл do-while и наоборот. Они делают разные вещи. И в этом случае вызовы нескольких методов будут полностью доминировать над тем, что сделал компилятор с while по сравнению с do-while.

Ответ 5

Замечание не о выборе управляющего оператора (do vs. while), а о разворачивании цикла!!!

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

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

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

Ответ 6

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

while (Condition)
{
}

и

do
{
}
while (Condition);

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