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