Каково различие между смещением бита и арифметическими операциями?

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
задан emenegro 7 June 2010 в 08:33
поделиться

10 ответов

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

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

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

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

18
ответ дан 3 December 2019 в 14:10
поделиться

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

0
ответ дан 3 December 2019 в 14:10
поделиться

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

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

1
ответ дан 3 December 2019 в 14:10
поделиться

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

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

.
0
ответ дан 3 December 2019 в 14:10
поделиться

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

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

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

4
ответ дан 3 December 2019 в 14:10
поделиться

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

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

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

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

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

8
ответ дан 3 December 2019 в 14:10
поделиться
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)

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

0
ответ дан 3 December 2019 в 14:10
поделиться

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

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

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

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

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

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

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

2
ответ дан 3 December 2019 в 14:10
поделиться

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

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

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

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

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

2
ответ дан 3 December 2019 в 14:10
поделиться

В качестве примера различий можно привести ассемблер 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;
1
ответ дан 3 December 2019 в 14:10
поделиться