Какой вариант лучше использовать для деления целого числа на 2?

367

Какой из следующих методов является наилучшим вариантом для деления целого на 2 и почему?

Техника 1:

x = x >> 1;

Техника 2:

x = x / 2;

Здесь x - целое число.

  • 0
    Подобный вопрос (но не дурак): stackoverflow.com/questions/1949271/…
  • 74
    Если вы действительно хотите снова присвоить результат x , то ни один из них не подходит для этого: он должен быть либо x >>= 1 либо x /= 2 , в зависимости от того, что вы намерены выразить с помощью операции. Не потому, что это быстрее (любой современный компилятор все равно скомпилирует все эквивалентные варианты в одинаковую, быструю сборку), а потому, что это менее запутанно.
Показать ещё 9 комментариев
Теги:
optimization
division
micro-optimization

23 ответа

834
Лучший ответ

Используйте операцию, которая лучше всего описывает то, что вы пытаетесь сделать.

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

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

-5 / 2  = -2
-5 >> 1 = -3

(идеон)

  • 20
    Первоначальный вопрос был также неопределенным в отношении термина «лучший». «Лучший» с точки зрения скорости, читабельности, экзаменационного вопроса, чтобы обмануть студентов и т. Д. ... В отсутствие объяснения того, что означает «лучший», это кажется наиболее правильным ответом.
  • 47
    В C ++ 03 оба являются реализацией, определенной для отрицательных чисел, и могут давать одинаковые результаты. В C ++ 11 деление хорошо определено для отрицательных чисел, но сдвиг все еще определяется реализацией.
Показать ещё 18 комментариев
211

Первый выглядит как деление? Нет. Если вы хотите разделить, используйте x / 2. Компилятор может оптимизировать его для использования бит-сдвига, если это возможно (это называется сокращением силы), что делает его бесполезной микро-оптимизацией, если вы сделаете это самостоятельно.

  • 14
    Многие компиляторы не превратят деление на степень двух в битовое смещение. Это было бы неправильной оптимизацией для целых чисел со знаком. Вы должны попытаться посмотреть на результаты сборки вашего компилятора и убедиться в этом сами.
  • 1
    IIRC Я использовал это для ускорения параллельного сокращения в CUDA (избегайте целочисленного div). Однако, это было больше года назад, мне интересно, насколько умны компиляторы CUDA в наши дни.
Показать ещё 3 комментария
186

Чтобы свалить: есть много причин, чтобы использовать x = x / 2; Вот некоторые из них:

  • он более четко выражает ваши намерения (предполагая, что вы не имеете дело с битами битов бит или чего-то еще)

  • компилятор все равно уменьшит это до операции переключения

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

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

    x = x / 2 + 5;
    x = x >> 1 + 5;  // not the same as above
    
  • подписанная арифметика может усложнить ситуацию даже больше, чем упомянутая выше проблема приоритета

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

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

  • 5
    Также стоит добавить, что, хотя существуют правила приоритета, нет ничего плохого в использовании скобок. Перерабатывая некоторый производственный код, я на самом деле увидел что-то вроде a/b/c*d (где a..d обозначает числовые переменные) вместо гораздо более читабельного (a*d)/(b*c) .
  • 1
    Производительность и оптимизация зависят от компилятора и цели. Например, я выполняю некоторую работу для микроконтроллера, где отключено что-либо выше, чем -O0, если вы не купите коммерческий компилятор, поэтому компилятор определенно не превратится в деление битов. Кроме того, битовые сдвиги занимают один цикл, а деление занимает 18 циклов на этой цели, и, поскольку тактовая частота микроконтроллеров довольно низкая, это может быть действительно заметным ударом по производительности (но это зависит от вашего кода - вы обязательно должны использовать /, пока профилирование не скажет Это проблема!)
Показать ещё 5 комментариев
59

Какой из них является лучшим вариантом и почему для деления целочисленного числа на 2?

Зависит от того, что вы подразумеваете под лучшим.

Если вы хотите, чтобы ваши коллеги ненавидели вас или чтобы ваш код сильно читался, я бы обязательно пошел с первым вариантом.

Если вы хотите разделить число на 2, перейдите со вторым.

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

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

59

Просто используйте divide (/), предполагая, что он более ясный. Соответственно будет оптимизирован компилятор.

  • 34
    Компилятор должен оптимизировать соответственно.
  • 12
    Если компилятор не оптимизирует соответственно, вы должны использовать лучший компилятор.
Показать ещё 2 комментария
38

Я согласен с другими ответами, что вы должны одобрить x / 2, потому что его намерение более ясное, и компилятор должен оптимизировать его для вас.

Однако другой причиной предпочтительности x / 2 over x >> 1 является то, что поведение >> зависит от реализации, если x является подписанным int и является отрицательным.

Из раздела 6.5.7, пуля 5 стандарта ISO C99:

Результатом E1 >> E2 является E1 смещение по правому краю E2. Если E1 имеет беззнаковый тип или если E1 имеет подписанный тип и неотрицательное значение, значение результата является неотъемлемой частью фактора E1/ 2 E2. Если E1 имеет подписанный тип и отрицательное значение, результирующее значение определяется реализацией.

  • 3
    Стоит отметить, что поведение, которое многие реализации определяют для x>>scalepower на отрицательных числах, будет именно тем, что необходимо при делении значения на степень два для таких целей, как рендеринг экрана, тогда как использование x/scalefactor будет неправильным, если только каждый применяет поправки к отрицательным значениям.
33

x / 2 более ясный, а x >> 1 не намного быстрее (согласно микро-бенчмарку, примерно на 30% быстрее для Java JVM). Как отмечали другие, для отрицательных чисел округление немного отличается, поэтому вы должны учитывать это, когда хотите обрабатывать отрицательные числа. Некоторые компиляторы могут автоматически преобразовывать x / 2 в x >> 1, если они знают, что число не может быть отрицательным (даже я не мог проверить это).

Даже x / 2 может не использовать инструкцию с медленным разделением CPU, потому что некоторые ярлыки возможны, но она все еще медленнее, чем x >> 1.

(Это вопрос на C/С++, другие языки программирования имеют больше операторов. Для Java также есть сдвиг без знака, x >>> 1, который снова отличается. Он позволяет правильно рассчитать среднее (среднее) значение два значения, так что (a + b) >>> 1 вернет среднее значение даже для очень больших значений a и b. Это требуется, например, для двоичного поиска, если индексы массива могут быть очень большими. Было ошибка во многих версиях бинарного поиска, потому что они использовали (a + b) / 2 для вычисления среднего значения.Это не работает правильно. Правильное решение заключается в использовании (a + b) >>> 1.)

  • 1
    Компиляторы не могут преобразовать x/2 в x>>1 в случаях, когда x может быть отрицательным. Если то, что нужно, это значение, которое x>>1 вычислит, это почти наверняка будет быстрее, чем любое выражение, использующее x/2 которое вычисляет то же значение.
  • 0
    Вы правы. Компиляторы могут конвертировать x/2 в x>>1 если он знает, что значение не является отрицательным. Я постараюсь обновить свой ответ.
Показать ещё 2 комментария
22

Кнут сказал:

Преждевременная оптимизация - это корень всего зла.

Поэтому я предлагаю использовать x /= 2;

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

  • 4
    Что бы вы назвали предпочтительным методом уменьшения числа на степень два, если хотите, чтобы целые числа поддерживали аксиому (которая применяется к натуральным числам и действительным числам), которая (n + d) / d = (n / d) + 1? Нарушения этой аксиомы при масштабировании графики приведут к видимым «швам» в результате. Если кто-то хочет что-то равномерное и почти симметричное относительно нуля, (n+8)>>4 работает хорошо. Можете ли вы предложить какой-либо подход, который был бы столь же ясным или эффективным без использования подписанного сдвига вправо?
17

Посмотрите на вывод компилятора, который поможет вам решить. Я проверил этот тест на x86-64 с помощью gcc (GCC) 4.2.1 20070719 [FreeBSD]

Также см. компилятор выводит онлайн в godbolt.

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

Код C со сборкой:

Для деления ваш вход будет

int div2signed(int a) {
  return a / 2;
}

и это компилируется в

    movl    %edi, %eax
    shrl    $31, %eax
    addl    %edi, %eax
    sarl    %eax
    ret

аналогично для сдвига

int shr2signed(int a) {
  return a >> 1;
}

с выходом:

    sarl    %edi
    movl    %edi, %eax
    ret
  • 0
    В зависимости от того, что вы делаете, это может исправить ошибку «один за другим», или это может вызвать ошибку «один за другим» (по сравнению с тем, что действительно необходимо), что потребует использования дополнительного кода для ее исправления. Если то, что кто-то хочет, является неудачным результатом, то сдвиг вправо быстрее и проще, чем любая другая альтернатива, о которой я знаю.
  • 0
    Если вам нужен пол, вряд ли вы бы описали то, что вы хотите, как «деление на 2»
Показать ещё 3 комментария
15

Просто добавленное примечание -

x * = 0.5 часто будет быстрее в некоторых языках на основе VM, в частности ActionScript, так как переменная не должна быть проверена для деления на 0.

  • 7
    Орли? jsperf.com/multiplication-vs-division
  • 4
    2 не является переменной.
Показать ещё 2 комментария
13

Используйте x = x / 2; ИЛИ x /= 2;. Возможно, что в будущем новый программист будет работать над этим. Поэтому ему будет легче узнать, что происходит в строке кода. Все могут не знать о такой оптимизации.

12

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

12

Я бы сказал, что есть несколько вещей, которые следует учитывать.

  • Bitshift должен быть быстрее, поскольку никакие специальные вычисления на самом деле   необходимо было сдвинуть бит, однако, как указано, есть   потенциальные проблемы с отрицательным числом. Если у вас есть   положительные числа, и ищут скорость, тогда я бы порекомендовал   Bitshift.

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

  • В зависимости от основного оборудования,       операции могут иметь разные скорости. Amdal закон должен сделать       обычный случай быстро. Таким образом, у вас может быть оборудование, которое может выполнять       разные операции быстрее других. Например, умножая на       0.5 может быть быстрее, чем деление на 2. (Конечно, вам может потребоваться взять слово умножения, если вы хотите применить целочисленное деление).

Если вы после чистой производительности, я бы рекомендовал создать несколько тестов, которые могли бы выполнять операции миллионы раз. Попробуйте выполнить несколько раз (размер выборки), чтобы определить, какой из них статистически лучше всего подходит для вашей OS/Hardware/Compiler/Code.

  • 2
    «Bitshift должен быть быстрее». компиляторы оптимизируют деление на битовые сдвиги
  • 0
    Я надеюсь, что они будут, но если у вас нет доступа к источнику компилятора, вы не можете быть уверены :)
Показать ещё 1 комментарий
12

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

x → 1 будет лучше, чем x/2. Я проверил на ideone.com, запустив программу, где произошло более 10 ^ 10 делений на 2 операции. x/2 заняло почти 5,5 с, тогда как x → 1 заняло около 2,6 с для той же программы.

  • 1
    Для значений без знака компилятор должен оптимизировать x/2 до x>>1 . Для значений со знаком почти все реализации определяют x>>1 чтобы иметь значение, которое эквивалентно x/2 но может быть вычислено быстрее, когда x положительно, и полезно отличается от x/2 когда x отрицательно.
11

Сделайте ваши намерения более ясными... например, если вы хотите разделить, используйте x/2, и пусть компилятор оптимизирует его для переключения оператора (или чего-то еще).

Сегодня процессоры не позволят этим оптимизации влиять на производительность ваших программ.

11

x = x/2; это подходящий код для использования. но операция зависит от вашей собственной программы того, как вы хотите произвести вывод.

10

Ответ на это будет зависеть от среды, в которой вы работаете.

  • Если вы работаете с 8-разрядным микроконтроллером или чем-либо без аппаратной поддержки для умножения, ожидается, что смещение битов будет обычным явлением, и хотя компилятор почти наверняка превратит x /= 2 в x >>= 1, наличие деления символ будет поднимать больше бровей в этой среде, чем использовать сдвиг, чтобы произвести деление.
  • Если вы работаете в критичной для производительности среде или разделе кода, или ваш код может быть скомпилирован с оптимизацией компилятора, x >>= 1 с комментарием, объясняющим его аргументацию, вероятно, лучше всего для ясности цели.
  • Если вы не находитесь в одном из вышеуказанных условий, сделайте свой код более удобным для чтения, просто используя x /= 2. Лучше сохранить следующего программиста, который, случается, смотрит на ваш код 10-секундного двойного действия на вашу операцию переключения, чем без необходимости доказывать, что вы знали, что смена была более эффективной без оптимизации компилятора.

Все они предполагают целые числа без знака. Простой сдвиг, вероятно, не тот, который вы хотите для подписания. Кроме того, DanielH очень хорошо разбирается в использовании x *= 0.5 для определенных языков, таких как ActionScript.

8

mod 2, test for = 1. dunno синтаксис в c. но это может быть самым быстрым.

7

Очевидно, что если вы пишете свой код для следующего парня, который его читает, идите за ясность "x/2".

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

Когда я действительно приурочил оба способа, я с удивлением обнаружил, что x/2 быстрее, чем x → 1

Это использовало Microsoft VS2008 С++ с включенной оптимизацией по умолчанию.

7

общий правый сдвиг делится:

q = i >> n; is the same as: q = i / 2**n;

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

Взгляните на эту страницу из Практического программирования на C++.

  • 0
    Если кто-то хочет вычислить значение, которое, например, (x+128)>>8 вычислило бы для значений x не близких к максимуму, как можно было бы сделать это кратко без сдвига? Обратите внимание, что (x+128)/256 не будет работать. Вы знаете какое-нибудь хорошее выражение, которое будет?
4

С точки зрения производительности. Операции смены процессора значительно быстрее, чем деактивировать op-коды. Таким образом, разделение на два или умножение на 2 и т.д. Все выгоды от операций смены.

Что касается внешнего вида. Как инженеры, когда мы стали настолько привязаны к косметике, что даже прекрасные дамы не используют!:)

3

X/Y - правильный оператор... и " → ". Если мы хотим, чтобы два разделили целое число, мы можем использовать (/) оператор дивидендов. оператор сдвига используется для сдвига бит.

х = х /2; х/= 2; мы можем так использовать.

0

В то время как x → 1 быстрее, чем x/2, правильное использование → при работе с отрицательными значениями немного сложнее. Для этого требуется нечто похожее на следующее:

// Extension Method
public static class Global {
    public static int ShiftDivBy2(this int x) {
        return (x < 0 ? x + 1 : x) >> 1;
    }
}
Сообщество Overcoder
Наверх
Меню