Странное поведение правого оператора сдвига (1 >> 32) - PullRequest
21 голосов
/ 03 августа 2010

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

Следующая программа:

#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <stdint.h>

int foo(int a, int b)
{
   return a >> b;
}

int bar(uint64_t a, int b)
{
   return a >> b;
}

int main(int argc, char** argv)
{
    std::cout << "foo(1, 32): " << foo(1, 32) << std::endl;
    std::cout << "bar(1, 32): " << bar(1, 32) << std::endl;
    std::cout << "1 >> 32: " << (1 >> 32) << std::endl; //warning here
    std::cout << "(int)1 >> (int)32: " << ((int)1 >> (int)32) << std::endl; //warning here

    return EXIT_SUCCESS;
}

Выходы:

foo(1, 32): 1 // Should be 0 (but I guess I'm missing something)
bar(1, 32): 0
1 >> 32: 0
(int)1 >> (int)32: 0

Что происходит с функцией foo()? Я понимаю, что единственное различие между тем, что он делает, и последними двумя строками состоит в том, что последние две строки оцениваются во время компиляции. И почему это "работает", если я использую 64-битное целое число?

Будем очень благодарны за любые огни!


Конечно, вот что дает g++:

> g++ -o test test.cpp
test.cpp: In function 'int main(int, char**)':
test.cpp:20:36: warning: right shift count >= width of type
test.cpp:21:56: warning: right shift count >= width of type

Ответы [ 7 ]

35 голосов
/ 03 августа 2010

Вероятно, ЦП на самом деле вычисляет

a >> (b % 32)

в foo; между тем, 1 >> 32 является константным выражением, поэтому компилятор сворачивает константу во время компиляции, что так или иначе дает 0.

Поскольку стандарт (C ++ 98 и раздел 5.8 / 1) гласит, что

Поведение не определено , если правый операнд отрицательный или больше или равен длине в битах повышенного левого операнда.

Нет противоречия, если foo(1,32) и 1>>32 дают разные результаты.

С другой стороны, в bar вы указали 64-битное значение без знака, так как 64> 32 гарантирует, что результат должен быть 1/2 32 = 0 Тем не менее, если вы напишите

bar(1, 64);

вы все равно можете получить 1.


Редактировать: логический сдвиг вправо (SHR) ведет себя как a >> (b % 32/64) на x86 / x86-64 (Intel # 253667, стр. 4-404):

Операндом-адресатом может быть регистр или ячейка памяти. Операндом count может быть непосредственное значение или регистр CL. Счет замаскирован до 5 бит (или 6 бит, если в 64-битном режиме используется REX.W). Диапазон счетчиков ограничен от 0 до 31 (или 63, если в 64-битном режиме и REX .W используется). Для счетчика 1 предусмотрена специальная кодировка кода операции.

Однако в ARM (как минимум, armv6 и 7) логический сдвиг вправо (LSR) реализован как (ARMISA Страница A2-6)

(bits(N), bit) LSR_C(bits(N) x, integer shift)
    assert shift > 0;
    extended_x = ZeroExtend(x, shift+N);
    result = extended_x<shift+N-1:shift>;
    carry_out = extended_x<shift-1>;
    return (result, carry_out);

где (ARMISA Page AppxB-13)

ZeroExtend(x,i) = Replicate('0', i-Len(x)) : x

Это гарантирует, что смещение вправо ≥32 приведет к нулю. Например, когда этот код запускается на iPhone, foo(1,32) выдаст 0.

Это показывает, что сдвиг 32-разрядного целого числа на ≥32 непереносим.

6 голосов
/ 03 августа 2010

OK.Таким образом, это в 5.8.1:

Операнды должны быть целочисленного типа или типа перечисления, и выполняются интегральные преобразования.Тип результата - тип повышенного левого операнда.Поведение не определено, если правый операнд отрицательный или больше или равен длине в битах повышенного левого операнда.

Таким образом, у вас есть неопределенное поведение (tm).

3 голосов
/ 03 августа 2010

Что происходит в foo, так это то, что ширина смещения больше или равна размеру смещаемых данных.В стандарте C99 это приводит к неопределенному поведению.Вероятно, это то же самое в любом стандарте C ++, на котором построен MS VC ++.

Причина этого заключается в том, чтобы позволить разработчикам компиляторов использовать преимущества любой аппаратной поддержки ЦП для смен.Например, архитектура i386 имеет инструкцию сдвигать 32-битное слово на количество битов, но количество битов определяется в поле в инструкции шириной 5 бит.Скорее всего, ваш компилятор генерирует инструкцию, беря величину сдвига битов и маскируя ее с 0x1F, чтобы получить сдвиг битов в инструкции.Это означает, что сдвиг на 32 - это то же самое, что сдвиг на 0.

1 голос
/ 03 августа 2010

Я скомпилировал его на 32-битных окнах, используя компилятор VC9. Это дало мне следующее предупреждение . Поскольку sizeof(int) на моем системном компиляторе составляет 4 байта, это означает, что смещение вправо на 32 бита приводит к неопределенному поведению. Поскольку он не определен, вы не можете предсказать результат. Просто для проверки я сместился вправо с 31 битом, и все предупреждения исчезли, и результат также был ожидаемым (то есть 0).

0 голосов
/ 03 августа 2010

foo (1,32) выполняет rotate-shit, поэтому биты, которые должны исчезнуть справа, снова появляются слева.Если вы сделаете это 32 раза, единственный бит, установленный в 1, вернется к своей исходной позиции.

бар (1,32) такой же, но бит находится в 64-32 + 1 = 33-й бит, что выше представимых чисел для 32-битного int.Только 32 младших бита берутся, и все они равны 0.

1 >> 32 выполняется компилятором.Понятия не имею, почему gcc использует здесь не вращающийся сдвиг, а не в сгенерированном коде.

То же самое для ((int) 1 >> (int) 32)

0 голосов
/ 03 августа 2010

Предупреждение говорит само за себя!

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

int a = 1;
cout << ( a >> 32);

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

0 голосов
/ 03 августа 2010

Полагаю, причина в том, что тип int содержит 32 бита (для большинства систем), но один бит используется для знака, поскольку это тип со знаком.Таким образом, для фактического значения используются только 31 бит.

...