Перейти к содержанию

Арифметический сдвиг

Сдвиг вправо на один разряд делит число на два — но только если правильно обойтись со знаком. Логический сдвиг вдвигает слева ноль и портит отрицательные числа, а арифметический повторяет знаковый бит, и −4 превращается в −2.

Обновлено Редакция GAW

Арифметический сдвиг (arithmetic shift) — сдвиг двоичного числа со знаком, при котором знак сохраняется. При сдвиге вправо в старший разряд вдвигается копия знакового бита, а не ноль, поэтому для чисел в дополнительном коде сдвиг на n разрядов вправо — деление на 2n с округлением вниз. Сдвиг влево у арифметического и логического вариантов одинаков: справа вдвигаются нули.

Пример в 8 битах

ЧислоКодLSR (логический)ASR (арифметический)
160x100x08 = 80x08 = 8
−40xFC0x7E = 1260xFE = −2
−50xFB0x7D = 1250xFD = −3

Пример с 0x10 и 0xFC — из руководства AVR (команда ASR), строка −5 — наш расчёт. Видно, что ASR округляет к минус бесконечности: −5 / 2 = −2,5 превращается в −3, тогда как деление / в C округляет к нулю и даёт −2.

Команды процессоров

  • AVR: ASR сдвигает регистр вправо, бит 7 сохраняется, бит 0 уходит во флаг переноса — Microchip отмечает, что по нему можно округлить результат. LSR — логический сдвиг, в бит 7 вдвигается 0.
  • Cortex-M: ASR и LSR с непосредственным сдвигом на 1–32 разряда; в форме с суффиксом S (ASRS, LSRS) флаг C получает последний выдвинутый бит, флаг V не меняется.

В языке C

По стандарту C11 (6.5.7) сдвиг вправо отрицательного знакового числа определяется реализацией, а сдвиг на отрицательное число или на ширину типа и больше — неопределённое поведение. GCC, по своей документации (Integers implementation), выполняет такой сдвиг с расширением знака, то есть как ASR, — см. статью о битовых операциях. Но для переносимого кода сдвигают беззнаковые значения, а знаковое деление пишут явно.

Не путать арифметический сдвиг с циклическим (ROR/ROL), при котором выдвинутый бит возвращается с другой стороны. Сдвиги и флаги AVR и Cortex-M показывает калькулятор двоичной арифметики, маски и сдвиги в C — в статье «Битовые операции в C».

Частые вопросы

Чем арифметический сдвиг отличается от логического?

Логический сдвиг вправо (LSR) вдвигает в старший бит ноль, арифметический (ASR) повторяет старый знаковый бит. Для положительных чисел результат одинаков, а для отрицательных логический сдвиг даёт большое положительное число: 0xFC (−4) после LSR — 0x7E = 126, после ASR — 0xFE = −2.

Делит ли арифметический сдвиг на 2 точно?

Для чётных чисел — да. Для нечётных отрицательных результат округляется к минус бесконечности: ASR(−5) даёт −3 (в GCC так же работает −5 >> 1, см. ниже), а деление −5 / 2 в C даёт −2, потому что оно округляет к нулю (наш расчёт по определениям). Выдвинутый бит у AVR попадает во флаг C, и по нему можно скорректировать округление.

Какой сдвиг делает >> в C для отрицательных чисел?

Стандарт C11 (6.5.7) оставляет это на усмотрение реализации: результат сдвига вправо отрицательного знакового числа определяется компилятором. Чтобы не зависеть от этого, сдвигают беззнаковые значения или делят явно.

Источники

  1. Microchip. AVR Instruction Set Manual, DS40002198B, 2021 — 6.6 ASR: все биты сдвигаются вправо, бит 7 сохраняется, бит 0 уходит во флаг C; фактически деление числа со знаком на 2 без изменения знака, по C можно округлить результат; V = N ⊕ C; пример 0x10 → 8, 0xFC (−4) → −2; 6.74 LSR: в бит 7 вдвигается 0, N = 0
  2. Arm. Armv7-M Architecture Reference Manual, ARM DDI 0403E.e — A2.2: псевдокод ASR_C, LSR_C — флаг C получает последний выдвинутый бит; A7.7.10 ASR (immediate): сдвиг на 1–32, флаг V не меняется
  3. GCC Manual: Integers implementation — сдвиг вправо отрицательного знакового числа GCC выполняет с расширением знака
  4. ISO/IEC 9899:201x (C11), Committee Draft N1570, 2011 — 6.5.7: целочисленные продвижения операндов; сдвиг на отрицательное число или не меньше ширины — неопределённое поведение; E1 >> E2 для отрицательного E1 со знаком — определяется реализацией

Нашли ошибку?