Bezdepbonusu7Программирование: статьи и ответы

Вопрос

Вы бы использовали num% 2 или num & 1, чтобы проверить, четное ли число?

Автор вопроса: rmnОтветов: 12

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

 1. if (num%2 == 0) { /* even */ }
 2. if ((num&1) == 0) { /* even */ }

Я считаю второй вариант гораздо более элегантным и значимым, и я обычно его использую. Но дело не только в вкусе; Фактическая производительность может отличаться: обычно побитовые операции (такие как logial-and here) намного эффективнее, чем операция mod (или div). Конечно, вы можете возразить, что некоторые компиляторы все равно смогут его оптимизировать, и я согласен ... но некоторые нет.

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

Что вы думаете?

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

  • Используйте тот, который легче всего читать. Вы не должны заботиться о производительности. Это работа компилятора. Готов поспорить, что после оптимизации получившаяся сборка будет точно такой же. Martin York 23.12.2009
  • Я просто, хотя (& 1) может не работать с отрицательными числами. Это будет зависеть от того, использует ли реализация 1-комплимент или 2-комплимент. Martin York 23.12.2009
  • Мартин, ты прав по обоим пунктам. Фактический результат / и% также определяется реализацией, если хотя бы один аргумент отрицательный. Хотя в этом случае все нормально. C ++ 0x примет правило C99, согласно которому целочисленное деление всегда округляется до нуля. sellibitze 23.12.2009
  • Я предполагаю, что все возненавидят меня за то, что я даже предложил (! (N% 2)) {;} silentcontributor 23.12.2009
  • это, вероятно, только принесет пользу всем, если эти программисты найдут такое короткое время, чтобы понять утверждения такого рода. Именно такие представления приводят к эзотерическому неподдерживаемому коду. Практически всегда лучше использовать более удобочитаемый подход, чем подход с более высокой производительностью. RJFalconer 23.12.2009
  • Есть ли какой-нибудь компилятор, написанный после 1980 года, который не генерировал бы один и тот же код для двух операторов? (предлагается дополнение до двух, а те, конечно, не будут, но действительно ли есть компиляторы / чипы, которые не используют дополнение до двух?) erikkallen 23.12.2009

c++numbersreadabilitylow-levelbitwise-operators

Ответы

Ответ 1 · рейтинг 24 · Steve Jessop

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

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

Конечно, если num относится к беззнаковому типу или к одному из целочисленных типов фиксированной ширины C99 (int8_t и т. Д., Которые должны быть дополнением до 2), то это не проблема. В этом случае я считаю %2 более элегантным и значимым, а &1 - хакерским приемом, который иногда может быть необходим для повышения производительности. Я думаю, например, что CPython не выполняет эту оптимизацию, и то же самое будет верно для полностью интерпретируемых языков (хотя тогда накладные расходы на синтаксический анализ, вероятно, затмевают разницу между двумя машинными инструкциями). Я был бы немного удивлен, если бы встретил компилятор C или C ++, который не делал этого там, где это было возможно, потому что это не проблема в момент выдачи инструкций, если не раньше.

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

Ответ 2 · рейтинг 76 · jason

Сначала я кодирую для удобства чтения, поэтому мой выбор здесь num % 2 == 0. Это гораздо яснее, чем num & 1 == 0. Я позволю компилятору позаботиться об оптимизации за меня и буду настраивать его только в том случае, если профилирование покажет, что это узкое место. Все остальное преждевременно.

Считаю второй вариант гораздо более элегантным и содержательным.

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

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

И, что более важно, детали должны быть скрыты с помощью isEven метода. Таким образом, мы должны видеть if(isEven(someNumber)) { // details } и видеть num % 2 == 0 только один раз в определении isEven.

Ответ 3 · рейтинг 12 · Doug T.

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

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

Ответ 4 · рейтинг 12 · AnT

Ваш вывод о производительности основан на популярной ложной предпосылке.

По какой-то причине вы настаиваете на переводе языковых операций в их "очевидные" машинные аналоги и делаете выводы о производительности на основе этого перевода. В этом конкретном случае вы пришли к выводу, что побитовая и & операция языка C ++ должна быть реализована с помощью машинной операции побитовой и, тогда как операция по модулю % должна каким-то образом включать машинное деление em >, что якобы медленнее. Такой подход имеет очень ограниченное применение, если вообще используется.

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

Когда дело доходит до таких базовых операций с непосредственной константой в качестве операнда, любой уважающий себя компилятор всегда сразу "поймет", что и num & 1, и num % 2 для интегрального num делают одно и то же, что заставит компилятор сгенерировать абсолютно идентичный код для оба выражения. Естественно, производительность будет точно такой же.

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

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

Другими словами, аргумент эффективности неверен.

Во-вторых, возвращаясь к исходному вопросу, более предпочтительным способом определения четности / нечетности значения, безусловно, является подход num % 2, поскольку он реализует требуемую проверку буквально ("по определению") и четко выражает Дело в том, что проверка чисто математическая. Т.е. он дает понять, что мы заботимся о свойстве числа, а не о свойстве его представления (как это было бы в случае варианта num & 1).

Вариант num & 1 следует зарезервировать для ситуаций, когда вам нужен доступ к битам представления значения числа. Использование этого кода для проверки четности / нечетности - весьма сомнительная практика.

Ответ 5 · рейтинг 9 · mrkj

Неоднократно упоминалось, что любой современный компилятор создаст одну и ту же сборку для обоих вариантов. Это напомнило мне демонстрационную страницу LLVM, которую я где-то видел на днях, поэтому решил, что дам ее тому назад. Я знаю, что это не более чем анекдотический случай, но он подтверждает то, что мы ожидали: x%2 и x&1 реализованы одинаково.

Я также попытался скомпилировать оба из них с помощью gcc-4.2.1 (gcc -S foo.c), и полученная сборка идентична (и вставлена ​​внизу этого ответа).

Запрограммируйте первое:

int main(int argc, char **argv) {
  return (argc%2==0) ? 0 : 1;
}

Результат:

; ModuleID = '/tmp/webcompile/_27244_0.bc'
target datalayout = "e-p:32:32:32-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:32:64-f32:32:32-f64:32:64-v64:64:64-v128:128:128-a0:0:64-f80:32:32"
target triple = "i386-pc-linux-gnu"

define i32 @main(i32 %argc, i8** nocapture %argv) nounwind readnone {
entry:
    %0 = and i32 %argc, 1       ; <i32> [#uses=1]
    ret i32 %0
}

Запрограммируйте второе:

int main(int argc, char **argv) {
  return ((argc&1)==0) ? 0 : 1;
}

Результат:

; ModuleID = '/tmp/webcompile/_27375_0.bc'
target datalayout = "e-p:32:32:32-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:32:64-f32:32:32-f64:32:64-v64:64:64-v128:128:128-a0:0:64-f80:32:32"
target triple = "i386-pc-linux-gnu"

define i32 @main(i32 %argc, i8** nocapture %argv) nounwind readnone {
entry:
    %0 = and i32 %argc, 1       ; <i32> [#uses=1]
    ret i32 %0
}

Вывод GCC:

.text
.globl _main
_main:
LFB2:
  pushq %rbp
LCFI0:
  movq  %rsp, %rbp
LCFI1:
  movl  %edi, -4(%rbp)
  movq  %rsi, -16(%rbp)
  movl  -4(%rbp), %eax
  andl  $1, %eax
  testl %eax, %eax
  setne %al
  movzbl  %al, %eax
  leave
  ret
LFE2:
  .section __TEXT,__eh_frame,coalesced,no_toc+strip_static_syms+live_support
EH_frame1:
  .set L$set$0,LECIE1-LSCIE1
  .long L$set$0
LSCIE1:
  .long 0x0
  .byte 0x1
  .ascii "zR\0"
  .byte 0x1
  .byte 0x78
  .byte 0x10
  .byte 0x1
  .byte 0x10
  .byte 0xc
  .byte 0x7
  .byte 0x8
  .byte 0x90
  .byte 0x1
  .align 3
LECIE1:
.globl _main.eh
_main.eh:
LSFDE1:
  .set L$set$1,LEFDE1-LASFDE1
  .long L$set$1
ASFDE1:
  .long LASFDE1-EH_frame1
  .quad LFB2-.
  .set L$set$2,LFE2-LFB2
  .quad L$set$2
  .byte 0x0
  .byte 0x4
  .set L$set$3,LCFI0-LFB2
  .long L$set$3
  .byte 0xe
  .byte 0x10
  .byte 0x86
  .byte 0x2
  .byte 0x4
  .set L$set$4,LCFI1-LCFI0
  .long L$set$4
  .byte 0xd
  .byte 0x6
  .align 3
LEFDE1:
  .subsections_via_symbols

Ответ 6 · рейтинг 7 · Bill Forster

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

ОДНАКО: у вашего лайнера есть ошибка.

Вы должны идти

if( (x&1) == 0 )

нет

if( x&1 == 0 )

Последний выполняет операцию AND x с 1 == 0, т. Е. Он выполняет операцию AND x с 0, получая 0, что, конечно, всегда считается ложным.

Итак, если вы сделали именно так, как вы предлагаете, все числа нечетные!

Ответ 7 · рейтинг 3 · Peter Alexander

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

Я бы сказал, что использование модуля по модулю упростит понимание, но создание функции is_even, использующей метод x & 1, дает вам лучшее из обоих миров.

Ответ 8 · рейтинг 3 · Jon Seigel

Они оба довольно интуитивны.

Я бы дал небольшое преимущество num % 2 == 0, но на самом деле у меня нет предпочтений. Конечно, что касается производительности, это, вероятно, микрооптимизация, поэтому я бы не стал об этом беспокоиться.

Ответ 9 · рейтинг 2 · Cody Gray

Я потратил годы, настаивая на том, что любой разумный компилятор, стоящий занимаемого им места на диске, оптимизирует num % 2 == 0 до num & 1 == 0. Затем, проанализировав разборку по другой причине, у меня была возможность действительно проверить свое предположение.

Оказывается, я ошибался. Microsoft Visual Studio, вплоть до версии 2013, генерирует следующий объектный код для num % 2 == 0:

    and ecx, -2147483647        ; the parameter was passed in ECX
    jns SHORT $IsEven
    dec ecx
    or  ecx, -2
    inc ecx
$IsEven:
    neg ecx
    sbb ecx, ecx
    lea eax, DWORD PTR [ecx+1]

Да, в самом деле. Это в режиме выпуска со всеми включенными оптимизациями. Вы получаете практически одинаковые результаты при сборке для x86 или x64. Вы, наверное, не поверите мне; Я сам с трудом верил в это.

По сути, он делает то, что вы ожидаете от num & 1 == 0:

not  eax                        ; the parameter was passed in EAX
and  eax, 1

Для сравнения: GCC (начиная с версии 4.4) и Clang (начиная с версии 3.2) делают то, что можно было бы ожидать, создавая идентичный объектный код. для обоих вариантов. Однако согласно Интерактивный компилятор Мэтта Годболта, ICC 13.0.1 также не оправдывает моих ожиданий.

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

Но, как сказал Дуг Т., вероятно, лучше всего определить в вашей библиотеке функцию IsEven, которая получит все эти исправьте мелкие детали, чтобы вам больше не приходилось думать о них, и сохраняйте читабельность кода. Если вы регулярно нацеливаетесь на MSVC, возможно, вы определите эту функцию, как я:

bool IsEven(int value)
{
    const bool result = (num & 1) == 0;
    assert(result == ((num % 2) == 0));
    return result;
}

Ответ 10 · Thomas Bonini

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

Во всяком случае, я считаю, что 2) намного быстрее, так как не требует деления.

Ответ 11 · Frunsi

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

Компилятор может заменить версию по модулю эффективной версией. Но это звучит как предлог для предпочтения модулю.

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

При выводе значения, стоящего за утверждениями, может быть даже проще прочитать двоичную версию:

if( ( num & 1 ) == 0 ) { /* even */ }
if( ( 00010111b & 1 ) == 0 ) { /* even */ }
if( ( 00010110b & 1 ) == 0 ) { /* odd */ }

(Я использовал суффикс "b" только для пояснения, это не C / C ++)

С версией по модулю вы должны дважды проверить, как операция определена в ее деталях (например, проверьте документацию, чтобы убедиться, что 0 % 2 - это то, что вы ожидаете).

Бинарный AND проще и в нем нет двусмысленностей!

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

Ответ 12 · рейтинг -2 · A. Walton

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

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

Перевод вопроса и ответов сообщества Stack Overflow, лицензия CC BY-SA. Оригинал на Stack Overflow.

Похожие материалы