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.
То, каков "лучший" путь, должно сделать операции? Когда лучше для использования разрядного смещения?
Эти два функционально эквивалентны в приведенных вами примерах (за исключением последнего, который должен читать aValue * 8 == aValue << 3
), если вы используете положительные целые числа . Это только в случае умножения или деления на степени 2.
Битовый сдвиг никогда не бывает медленнее, чем арифметический. В зависимости от вашего компилятора арифметическая версия может быть скомпилирована до версии с битовым сдвигом, и в этом случае они обе будут столь же эффективными. В противном случае битовый сдвиг должен быть значительно быстрее, чем арифметический.
Однако арифметическая версия часто более удобочитаема. Следовательно, я использую арифметическую версию почти во всех случаях и использую сдвиг битов только в том случае, если профилирование показывает, что оператор находится в узком месте:
Программы должны быть написаны для чтения людьми и только случайно для выполнения машинами.
Если у вас большие вычисления в среде с замкнутым циклом, где скорость вычислений влияет на скорость --- используйте битовые операции. (считается быстрее, чем арифметические операции)
Пока вы умножаете или делите в пределах двух степеней, быстрее работать со сдвигом, потому что это единственная операция (требуется только один цикл процесса).
Привыкает читать << 1 как * 2 и >> 2 как / 4 довольно быстро, поэтому я не согласен с потерей читаемости при использовании сдвига, но это зависит от каждого человека.
Если вы хотите узнать больше о том, как и почему, возможно, вам поможет Википедия, или если вы хотите пройти сборку изучения боли; -)
Когда речь идет о числах степени 2 (2^x), лучше использовать сдвиг - он просто "сдвигает" биты. (1 операция ассемблера вместо 2 при делении).
Есть ли какой-нибудь язык, компилятор которого делает такую оптимизацию?
.Сдвиг битов - это операция "близко к металлу", которая в большинстве случаев не содержит никакой информации о том, чего вы действительно хотите добиться.
Если вы хотите разделить число на два, то, конечно, пишите x/2
. Бывает, что это достигается x >> 1
, но последнее скрывает намерение.
Когда это окажется узким местом, пересмотрите код.
Разница в том, что арифметические операции имеют четко определенные результаты (если они не приводят к подписанному переполнению, которое является). Во многих случаях операции сдвига не дают определенных результатов. Они четко определены для беззнаковых типов как в C, так и в C ++, но со знаковыми типами все быстро усложняется.
В языке C ++ арифметическое значение сдвига влево <<
для типов со знаком не определено. Он просто сдвигает биты, заполняя справа нулями. То, что это означает в арифметическом смысле, зависит от подписанного представления, используемого платформой. Практически то же самое верно и для оператора сдвига вправо >>
. Сдвиг вправо отрицательных значений приводит к результатам, определяемым реализацией.
В языке C все определяется немного по-другому. Сдвиг влево отрицательных значений невозможен: это приводит к неопределенному поведению. Сдвиг вправо отрицательных значений приводит к результатам, определяемым реализацией.
В большинстве практических реализаций каждый сдвиг вправо выполняет деление на 2 с округлением в сторону отрицательной бесконечности. Это, кстати, заметно отличается от арифметического деления /
на 2, поскольку обычно (и всегда в C99) время округляется до 0.
Что касается того, когда следует использовать битовый сдвиг. ... Битовый сдвиг предназначен для операций, которые работают с битами.Операторы битового сдвига очень редко используются в качестве замены арифметических операторов (например, вы никогда не должны использовать сдвиги для выполнения умножения / деления на константу).
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)
В зависимости от желаемого поведения округления вы можете предпочесть одно другому.
Какой "лучший" способ выполнять операции?
Используйте арифметические операции при работе с числами. При работе с битами используйте битовые операции. Период. Это здравый смысл. Я сомневаюсь, что кто-то когда-либо подумал бы, что использование операций сдвига битов для целых или двойных чисел в качестве обычной повседневной вещи - хорошая идея.
Когда лучше использовать сдвиг битов?
При работе с битами?
Дополнительный вопрос: одинаково ли они ведут себя в случае арифметического переполнения?
Да. Соответствующие арифметические операции (часто, но не всегда) упрощаются до их аналогов с битовым сдвигом большинством современных компиляторов.
Правка : Ответ был принят, но я просто хочу добавить, что в этом вопросе есть масса плохих советов. Вы никогда не должны (читайте: почти никогда) использовать операции битового сдвига при работе с целыми числами. Это ужасная практика.
Когда ваша цель - умножить несколько чисел, использование арифметических операторов имеет смысл.
Когда ваша цель состоит в том, чтобы фактически логически сдвинуть биты, используйте операторы сдвига.
Например, скажем, вы разделяете компоненты RGB из слова RGB, этот код имеет смысл:
int r,g,b;
short rgb = 0x74f5;
b = rgb & 0x001f;
g = (rgb & 0x07e0) >> 5;
r = (rgb & 0xf800) >> 11;
с другой стороны, когда вы хотите умножить какое-то значение на 4, вы действительно должны закодировать свое намерение, а не делать сдвиги .
В качестве примера различий можно привести ассемблер 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 $0x1f,%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 $0x1f,%eax
5c: c1 e8 1e shr $0x1e,%eax
5f: 01 d0 add %edx,%eax
61: c1 f8 02 sar $0x2,%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 $0x2,%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 $0x3,%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 $0x4,%eax
9a: c3 ret
Деление отличается - при представлении с дополнением два, сдвиг отрицательного нечетного числа вправо на единицу дает значение, отличное от деления на два. Но компилятор все равно оптимизирует деление до последовательности сдвигов и сложений.
Самое очевидное различие заключается в том, что эта пара не делает одно и то же - сдвиг на четыре эквивалентен умножению на шестнадцать, а не на восемь! Вы, вероятно, не получите ошибку, если позволите компилятору попотеть над небольшими оптимизациями за вас.
aNumber = aValue * 8;
aNumber = aValue << 4;