В чем разница между битовым сдвигом и арифметическими операциями?

int aNumber;

aNumber = aValue / 2;
aNumber = aValue >> 1;

aNumber = aValue * 2;
aNumber = aValue << 1;

aNumber = aValue / 4;
aNumber = aValue >> 2;

aNumber = aValue * 8;
aNumber = aValue << 3;

// etc.

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

10 ответов


эти два функционально эквивалентны в приведенных примерах (за исключением последнего, который должен читать aValue * 8 == aValue << 3), Если вы используете положительные целые числа. Это только в случае умножения или деления на степень 2.

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

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

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


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

в языке C++ арифметическое значение Left-shift << для подписанных типов не определено. Он просто сдвигает биты, заполняя нули справа. Что это значит в арифметический смысл зависит от подписанного представления, используемого платформой. Практически то же самое верно для right-shift >> оператора. Смещение вправо отрицательных значений приводит к результатам, определенным реализацией.

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

на большинстве практических реализаций каждый право смены выполняет деление на 2 с округлением в сторону отрицательной бесконечности. Это, кстати, заметно отличается от арифметического деления / на 2, так как обычно (и всегда в C99) времени он будет округляться в сторону 0.

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


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

Если вы хотите разделить число на два, то писать x/2. Это достигается x >> 1, но последнее скрывает намерение.

когда это окажется узким местом, пересмотрите код.


каков "лучший" способ выполнения операций?

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

когда лучше использовать сдвиг битов?

при работе с битами?

дополнительный вопрос: do они ведут себя одинаково в случае арифметического переполнения?

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

редактировать: ответ был принят, но я просто хочу добавить, что в этом вопросе есть тонна плохих советов. Вы никогда не должны (читать: почти никогда) использовать операции битового сдвига при работе с ints. Это ужасная практика.


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

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

например, скажем, вы разделяете компоненты RGB от слова RGB, этот код имеет смысл:

 int r,g,b;
 short rgb = 0x74f5;
 b = rgb & 0x001f;
 g = (rgb & 0x07e0) >> 5;
 r = (rgb & 0xf800) >> 11;

С другой стороны, когда вы хотите умножить некоторое значение на 4, Вы должны действительно кодировать свое намерение, а не делать сдвиги.


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

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


в качестве примера различий это сборка x86, созданная с помощью gcc 4.4 с -O3

int arithmetic0 ( int aValue )
{
    return aValue / 2;
}

00000000 <arithmetic0>:
   0:   55                      push   %ebp
   1:   89 e5                   mov    %esp,%ebp
   3:   8b 45 08                mov    0x8(%ebp),%eax
   6:   5d                      pop    %ebp
   7:   89 c2                   mov    %eax,%edx
   9:   c1 ea 1f                shr    x1f,%edx
   c:   8d 04 02                lea    (%edx,%eax,1),%eax
   f:   d1 f8                   sar    %eax
  11:   c3                      ret    

int arithmetic1 ( int aValue )
{
    return aValue >> 1;
}

00000020 <arithmetic1>:
  20:   55                      push   %ebp
  21:   89 e5                   mov    %esp,%ebp
  23:   8b 45 08                mov    0x8(%ebp),%eax
  26:   5d                      pop    %ebp
  27:   d1 f8                   sar    %eax
  29:   c3                      ret    

int arithmetic2 ( int aValue )
{
    return aValue * 2;
}

00000030 <arithmetic2>:
  30:   55                      push   %ebp
  31:   89 e5                   mov    %esp,%ebp
  33:   8b 45 08                mov    0x8(%ebp),%eax
  36:   5d                      pop    %ebp
  37:   01 c0                   add    %eax,%eax
  39:   c3                      ret    

int arithmetic3 ( int aValue )
{
    return aValue << 1;
}

00000040 <arithmetic3>:
  40:   55                      push   %ebp
  41:   89 e5                   mov    %esp,%ebp
  43:   8b 45 08                mov    0x8(%ebp),%eax
  46:   5d                      pop    %ebp
  47:   01 c0                   add    %eax,%eax
  49:   c3                      ret    

int arithmetic4 ( int aValue )
{
    return aValue / 4;
}

00000050 <arithmetic4>:
  50:   55                      push   %ebp
  51:   89 e5                   mov    %esp,%ebp
  53:   8b 55 08                mov    0x8(%ebp),%edx
  56:   5d                      pop    %ebp
  57:   89 d0                   mov    %edx,%eax
  59:   c1 f8 1f                sar    x1f,%eax
  5c:   c1 e8 1e                shr    x1e,%eax
  5f:   01 d0                   add    %edx,%eax
  61:   c1 f8 02                sar    x2,%eax
  64:   c3                      ret    

int arithmetic5 ( int aValue )
{
    return aValue >> 2;
}

00000070 <arithmetic5>:
  70:   55                      push   %ebp
  71:   89 e5                   mov    %esp,%ebp
  73:   8b 45 08                mov    0x8(%ebp),%eax
  76:   5d                      pop    %ebp
  77:   c1 f8 02                sar    x2,%eax
  7a:   c3                      ret    

int arithmetic6 ( int aValue )
{
    return aValue * 8;
}

00000080 <arithmetic6>:
  80:   55                      push   %ebp
  81:   89 e5                   mov    %esp,%ebp
  83:   8b 45 08                mov    0x8(%ebp),%eax
  86:   5d                      pop    %ebp
  87:   c1 e0 03                shl    x3,%eax
  8a:   c3                      ret    

int arithmetic7 ( int aValue )
{
    return aValue << 4;
}

00000090 <arithmetic7>:
  90:   55                      push   %ebp
  91:   89 e5                   mov    %esp,%ebp
  93:   8b 45 08                mov    0x8(%ebp),%eax
  96:   5d                      pop    %ebp
  97:   c1 e0 04                shl    x4,%eax
  9a:   c3                      ret    

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

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

aNumber = aValue * 8;
aNumber = aValue << 4;

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


когда речь идет о power 2 numbers (2^x), лучше использовать сдвиги - это просто "нажать" биты. (1 сборочная операция вместо 2 деления).

есть ли какой-либо язык, на котором его компилятор выполняет эту оптимизацию?


int i = -11;
std::cout << (i  / 2) << '\n';   // prints -5 (well defined by the standard)
std::cout << (i >> 1) << '\n';   // prints -6 (may differ on other platform)

в зависимости от желаемого поведения округления вы можете предпочесть одно другому.