Что происходит быстрее: while (1) или while (2)?

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

Что быстрее?

while(1) {
    // Some code
}

или

while(2) {
    //Some code
}

Я сказал, что обе имеют одинаковую скорость выполнения, так как выражение внутри while должно наконец оцениваться как true или false. В этом случае оба параметра оцениваются как true, и в условии while нет дополнительных условных инструкций. Таким образом, обе будут иметь одинаковую скорость выполнения, и я предпочитаю пока (1).

Но интервьюер уверенно сказал: "Проверьте свои основы. while(1) быстрее, чем while(2)". (Он не испытывал моей уверенности)

Это правда?

См. также: Is "for (;;)" быстрее, чем "while (TRUE)" ? Если нет, то почему люди используют его?

Ответ 1

Оба цикла бесконечны, но мы можем видеть, какой из них требуется больше инструкций/ресурсов на итерацию.

Используя gcc, я скомпилировал две следующие программы для сборки на разных уровнях оптимизации:

int main(void) {
    while(1) {}
    return 0;
}


int main(void) {
    while(2) {}
    return 0;
}

Даже без оптимизации (-O0), сгенерированная сборка была идентичной для обеих программ. Следовательно, между двумя циклами нет разницы в скорости.

Для справки, вот сгенерированная сборка (используя gcc main.c -S -masm=intel с флагом оптимизации):

С -O0:

    .file   "main.c"
    .intel_syntax noprefix
    .def    __main; .scl    2;  .type   32; .endef
    .text
    .globl  main
    .def    main;   .scl    2;  .type   32; .endef
    .seh_proc   main
main:
    push    rbp
    .seh_pushreg    rbp
    mov rbp, rsp
    .seh_setframe   rbp, 0
    sub rsp, 32
    .seh_stackalloc 32
    .seh_endprologue
    call    __main
.L2:
    jmp .L2
    .seh_endproc
    .ident  "GCC: (tdm64-2) 4.8.1"

С -O1:

    .file   "main.c"
    .intel_syntax noprefix
    .def    __main; .scl    2;  .type   32; .endef
    .text
    .globl  main
    .def    main;   .scl    2;  .type   32; .endef
    .seh_proc   main
main:
    sub rsp, 40
    .seh_stackalloc 40
    .seh_endprologue
    call    __main
.L2:
    jmp .L2
    .seh_endproc
    .ident  "GCC: (tdm64-2) 4.8.1"

С -O2 и -O3 (тот же вывод):

    .file   "main.c"
    .intel_syntax noprefix
    .def    __main; .scl    2;  .type   32; .endef
    .section    .text.startup,"x"
    .p2align 4,,15
    .globl  main
    .def    main;   .scl    2;  .type   32; .endef
    .seh_proc   main
main:
    sub rsp, 40
    .seh_stackalloc 40
    .seh_endprologue
    call    __main
.L2:
    jmp .L2
    .seh_endproc
    .ident  "GCC: (tdm64-2) 4.8.1"

Фактически, сборка, сгенерированная для цикла, одинакова для каждого уровня оптимизации:

 .L2:
    jmp .L2
    .seh_endproc
    .ident  "GCC: (tdm64-2) 4.8.1"

Важными битами являются:

.L2:
    jmp .L2

Я не могу хорошо прочитать сборку, но это, безусловно, безусловный цикл. Команда jmp безостановочно сбрасывает программу обратно на метку .L2, даже не сравнивая значение с истинным, и, разумеется, немедленно делает это снова, пока программа не закончится. Это напрямую соответствует коду C/С++:

L2:
    goto L2;

Edit:

Интересно, что даже без оптимизаций следующие петли генерировали точно такой же вывод (безусловный jmp) в сборке:

while(42) {}

while(1==1) {}

while(2==2) {}

while(4<7) {}

while(3==3 && 4==4) {}

while(8-9 < 0) {}

while(4.3 * 3e4 >= 2 << 6) {}

while(-0.1 + 02) {}

И даже к моему изумлению:

#include<math.h>

while(sqrt(7)) {}

while(hypot(3,4)) {}

Чем больше пользовательских функций, тем больше интереснее:

int x(void) {
    return 1;
}

while(x()) {}


#include<math.h>

double x(void) {
    return sqrt(7);
}

while(x()) {}

В -O0 эти два примера на самом деле вызывают x и выполняют сравнение для каждой итерации.

Первый пример (возврат 1):

.L4:
    call    x
    testl   %eax, %eax
    jne .L4
    movl    $0, %eax
    addq    $32, %rsp
    popq    %rbp
    ret
    .seh_endproc
    .ident  "GCC: (tdm64-2) 4.8.1"

Второй пример (возврат sqrt(7)):

.L4:
    call    x
    xorpd   %xmm1, %xmm1
    ucomisd %xmm1, %xmm0
    jp  .L4
    xorpd   %xmm1, %xmm1
    ucomisd %xmm1, %xmm0
    jne .L4
    movl    $0, %eax
    addq    $32, %rsp
    popq    %rbp
    ret
    .seh_endproc
    .ident  "GCC: (tdm64-2) 4.8.1"

Однако, при -O1 и выше, они оба создают ту же самую сборку, что и предыдущие примеры (безусловный jmp назад к предыдущей метке).

TL; DR

В GCC различные циклы скомпилированы для идентичной сборки. Компилятор оценивает постоянные значения и не пытается выполнить какое-либо фактическое сравнение.

Мораль истории:

  • Существует уровень перевода между исходным кодом С++ и инструкциями CPU, и этот уровень имеет важные последствия для производительности.
  • Таким образом, производительность не может быть оценена, только глядя на исходный код.
  • Компилятор должен быть достаточно умным, чтобы оптимизировать такие тривиальные случаи. Программисты не должны тратить свое время на размышления о них в подавляющем большинстве случаев.

Ответ 2

Да, while(1) намного быстрее, чем while(2), для человека, чтобы читать! Если я вижу while(1) в незнакомой кодовой базе, я сразу же знаю, что автор намеревался, и мои глазные яблоки могут перейти к следующей строке.

Если я увижу while(2), я, вероятно, остановлюсь на своем пути и попытаюсь понять, почему автор не написал while(1). Продвинулся ли автор на клавиатуре? Поддерживают ли поддерживающие эту кодовую базу while(n) как неясный механизм комментариев, чтобы сделать петли похожими? Это необоснованное обходное решение для ложного предупреждения в некотором сломанном статическом аналитическом инструменте? Или это ключ, который я читаю сгенерированный код? Является ли это ошибкой, вызванной плохой консультацией по поиску и замене, или плохим слиянием или космическим лучом? Возможно, эта строка кода должна делать что-то совершенно другое. Возможно, он должен был читать while(w) или while(x2). Мне лучше найти автора в истории файлов и отправить им электронную почту "WTF"... и теперь я нарушил свой ментальный контекст. while(2) может потреблять несколько минут моего времени, когда while(1) заняло бы долю секунды!

Я преувеличиваю, но немного. Чтение кода действительно важно. И это стоит упомянуть в интервью!

Ответ 3

Существующие ответы, показывающие код, сгенерированный конкретным компилятором для конкретной цели с определенным набором параметров, не полностью отвечают на вопрос - если вопрос не задан в этом конкретном контексте ( "Что быстрее использует gcc 4.7. 2 для x86_64 с параметрами по умолчанию?", Например).

Что касается определения языка, в абстрактной машине while (1) вычисляется целочисленная константа 1, а while (2) оценивает целочисленную константу 2; в обоих случаях результат сравнивается для равенства нулю. В стандарте языка абсолютно ничего не говорится об относительной производительности этих двух конструкций.

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

С другой стороны, компиляторы C абсолютно должны оценивать некоторые константные выражения во время компиляции, когда они появляются в контекстах, требующих постоянного выражения. Например, это:

int n = 4;
switch (n) {
    case 2+2: break;
    case 4:   break;
}

требуется диагностика; ленивый компилятор не имеет возможности отложить оценку 2+2 до времени выполнения. Поскольку компилятор должен иметь возможность оценивать постоянные выражения во время компиляции, нет веских оснований для этого не использовать эту возможность, даже если это не требуется.

Стандарт C (N1570 6.8.5p4) говорит, что

Оператор итерации вызывает оператор, называемый телом цикла выполняется до тех пор, пока контрольное выражение не сравнится с 0.

Таким образом, соответствующие константные выражения 1 == 0 и 2 == 0, оба из которых оцениваются значением int 0. (Эти сравнения неявны в семантике цикла while, они не существуют как фактические выражения C.)

Извращенный наивный компилятор может генерировать другой код для двух конструкций. Например, для первого он может генерировать безусловный бесконечный цикл (рассматривая 1 как частный случай), а для второго он может генерировать явное сравнение времени выполнения, эквивалентное 2 != 0. Но я никогда не сталкивался с компилятором C, который действительно будет вести себя таким образом, и я серьезно сомневаюсь, что такой компилятор существует.

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

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

Нижняя строка: while (1) и while(2) почти наверняка имеют одинаковую производительность. Они имеют точно такую ​​же семантику, и нет никаких оснований для того, чтобы какой-либо компилятор не генерировал идентичный код.

И хотя для компилятора совершенно законно генерировать более быстрый код для while(1), чем для while(2), он одинаково легален для компилятора генерировать более быстрый код для while(1), чем для другого вхождения while(1) в том же программа.

(В другом вопросе подразумевается тот вопрос, который вы задали: как вы относитесь к интервьюеру, который настаивает на неправильной технической точке. Это, вероятно, будет хорошим вопросом для сайта Workplace).

Ответ 4

Подождите минуту. Интервьюер, он был похож на этого парня?

enter image description here

Это достаточно плохо, что сам интервьюер провалил это интервью, что, если другие программисты этой компании "прошли" этот тест?

Нет. Оценка операторов 1 == 0 и 2 == 0 должна быть одинаковой быстротой. Мы могли представить себе плохие реализации компилятора, где можно было бы быстрее, чем другие. Но нет веской причины, почему нужно быть быстрее, чем другой.

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

Отказ от ответственности: Это НЕ оригинальный мультфильм Дилберта. Это всего лишь mashup.

Ответ 5

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

Кстати, если вы ответили

Оба фрагмента кода одинаково быстры, потому что оба требуют бесконечного времени для завершения

интервьюер скажет

Но while (1) может выполнять больше итераций в секунду; можете ли вы объяснить, почему? (это бессмыслица, снова проверяйте свою уверенность)

Таким образом, отвечая так же, как и вы, вы сэкономили время, которое в противном случае вы потратили бы на обсуждение этого плохого вопроса.


Вот пример кода, сгенерированного компилятором в моей системе (MS Visual Studio 2012), при отключении оптимизации:

yyy:
    xor eax, eax
    cmp eax, 1     (or 2, depending on your code)
    je xxx
    jmp yyy
xxx:
    ...

С включенными оптимизациями:

xxx:
    jmp xxx

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

Ответ 6

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

1 = 00000001
2 = 00000010

Если "равно нулю?" алгоритм начинается с правой стороны числа и должен проверять каждый бит до тех пор, пока он не достигнет ненулевого бита, цикл while(1) { } должен будет проверять в два раза больше битов на итерацию как цикл while(2) { }.

Это требует очень неправильной ментальной модели работы компьютеров, но у нее есть своя внутренняя логика. Один из способов проверки - спросить, будет ли while(-1) { } или while(3) { } одинаково быстро, или если while(32) { } будет еще медленнее.

Ответ 7

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

Они ввели вас в конфликт. Если это так, они умны, и вопрос был хорошим. Для некоторых отраслей, таких как банковское дело, размещение вашей проблемы в Stack переполнение может быть причиной отклонения.

Но, конечно, я не знаю, я просто предлагаю один вариант.

Ответ 8

Я думаю, что ключ должен быть найден в "спрошенном старшим менеджером". Этот человек, очевидно, перестал программировать, когда стал менеджером, а затем несколько лет занимал должность старшего менеджера. Никогда не терял интереса к программированию, но никогда не писал строки с тех пор. Поэтому его ссылка не является "достойным компилятором", как упоминают некоторые ответы, но "компилятор этого человека работал 20-30 лет назад".

В то время программисты потратили значительную часть своего времени на различные методы, чтобы сделать их код быстрее и эффективнее, так как процессорное время "центрального миникомпьютера" было настолько ценным. Как люди пишут компиляторы. Я предполагаю, что единственный в своем роде компилятор его компании в то время был оптимизирован на основе "часто встречающихся утверждений, которые могут быть оптимизированы", и немного поработал, когда встретился некоторое время (1) и оценил все else, включая некоторое время (2). Имея такой опыт, он мог объяснить свою позицию и уверенность в этом.

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

Ответ 9

Вы должны были спросить его, как он достиг этого вывода. Под любым достойным компилятором там два компилируются в те же инструкции asm. Итак, он должен был сказать вам компилятор, чтобы начать. И даже несмотря на это, вам нужно было бы хорошо знать компилятор и платформу, чтобы даже получить теоретическое обоснованное предположение. И, в конце концов, на практике это не имеет большого значения, поскольку существуют другие внешние факторы, такие как фрагментация памяти или системная нагрузка, которые будут влиять на цикл больше, чем эта деталь.

Ответ 10

Для этого вопроса я должен добавить, что я помню, как Дуг Гвин из C-комитета писал, что некоторые ранние компиляторы C без прохода оптимизатора генерируют тест в сборке для while(1) (по сравнению с for(;;), который wouldn это есть).

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

  • без использования оптимизатора компилятор генерирует тест как для while(1), так и while(2)
  • с передатчиком оптимизатора компилятору дается указание оптимизировать (с безусловным прыжком) все while(1), потому что они считаются идиоматическими. Это оставит while(2) с тестом и, следовательно, сделает разницу в производительности между этими двумя.

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

Ответ 11

Другой вопрос по этому вопросу должен состоять в том, чтобы увидеть, есть ли у вас смелость сообщить своему менеджеру, что он/она ошибается! И как мягко вы можете сообщить об этом.

Моим первым инстинктом было бы создание сборки, чтобы показать менеджеру, что любой достойный компилятор должен позаботиться об этом, и если он этого не сделает, вы отправите следующий патч для него:)

Ответ 12

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

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

Кроме того, я бы выбрал while(1) {}, потому что он более распространен, и другим товарищам по команде не нужно было тратить время на то, чтобы выяснить, почему кто-то пошел бы на большее число, чем 1.

Теперь напишите код.; -)

Ответ 13

Раньше я программировал код C и Assembly, когда такая глупость могла иметь значение. Когда это имело значение, мы написали его в Assembly.

Если бы меня спросили об этом вопросе, я бы повторил знаменитую цитату Дональда Кнута 1974 года о преждевременной оптимизации и пошел, если интервьюер не смеялся и не двигался дальше.

Ответ 14

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

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

Ответ 15

Здесь проблема: если вы на самом деле пишете программу и измеряете ее скорость, скорость обеих петель может быть разной! Для некоторого разумного сравнения:

unsigned long i = 0;
while (1) { if (++i == 1000000000) break; }

unsigned long i = 0;
while (2) { if (++i == 1000000000) break; }

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

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

Ответ 16

Оба они равны - то же самое.

В соответствии со спецификациями все, что не равно 0, считается истинным, поэтому даже без какой-либо оптимизации, а хороший компилятор не генерирует никакого кода (1) или while (2). Компилятор создаст простую проверку для != 0.

Ответ 17

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

И чтобы потратить на это еще больше времени...

", а (2)" смешно, потому что

"while (1)" и "while (true)" исторически используются для создания бесконечного цикла, который ожидает, что "break" будет вызываться на некотором этапе внутри цикла на основе условия, которое обязательно произойдет.

"1" просто существует, чтобы всегда оценивать true и, следовательно, говорить "while (2)" примерно так же глупо, как "while (1 + 1 == 2)", который также будет оцениваться как true.

И если вы хотите быть абсолютно глупым, просто используйте: -

while (1 + 5 - 2 - (1 * 3) == 0.5 - 4 + ((9 * 2) / 4)) {
    if (succeed())
        break;
}

Мне нравится думать, что ваш кодер сделал опечатку, которая не повлияла на работу кода, но если он намеренно использовал "2", чтобы просто быть странным, то уложите его, прежде чем он поместит странный sh! t весь ваш код с ним трудно читать и работать.

Ответ 18

Возможно, интервьюер намеренно задал такой тупой вопрос и хотел, чтобы вы сделали 3 очка:

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

Ответ 19

Это зависит от компилятора.

Если он оптимизирует код или оценивает значения 1 и 2 на true с тем же числом инструкций для определенного набора команд, скорость выполнения будет одинаковой.

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

Я имею в виду: это не вопрос, связанный с языком (C).

Ответ 20

Если вы обеспокоены оптимизацией, вы должны использовать

for (;;)

потому что у него нет тестов. (циничный режим)

Ответ 21

Единственная причина, по которой я могу думать о том, почему while(2) будет медленнее:

  • Код оптимизирует цикл до

    cmp eax, 2

  • Когда вычитание происходит, вы по существу вычитаете

    а. 00000000 - 00000010 cmp eax, 2

    вместо

    б. 00000000 - 00000001 cmp eax, 1

cmp устанавливает только флаги и не устанавливает результат. Итак, на наименее значимых битах мы знаем, нужно ли нам брать или нет с помощью b. Если a вам нужно выполнить две вычитания, прежде чем вы получите заем.