Почему 32-разрядные целые числа не срабатывают влево, сдвиг "<<", как ожидается, если их использовать более 32 раз?
Когда я пишу следующую программу и использую компилятор GNU C++, вывод 1
Я думаю, это связано с операцией вращения, выполняемой компилятором.
#include <iostream>
int main()
{
int a = 1;
std::cout << (a << 32) << std::endl;
return 0;
}
Но по логике, поскольку сказано, что биты теряются, если они переполняют ширину битов, вывод должен быть 0. Что происходит?
Код находится на ideone, http://ideone.com/VPTwj.
10 ответов
Это вызвано комбинацией неопределенного поведения в C и того факта, что код, сгенерированный для процессоров IA-32, имеет 5-битовую маску, примененную к счетчику сдвигов. Это означает, что на процессорах IA-32 диапазон сдвига составляет только 0-31. 1
Из языка программирования C 2
Результат не определен, если правый операнд отрицательный или больше или равен числу битов в типе левого выражения.
Из IA-32 Руководство разработчика программного обеспечения для архитектуры Intel 3
8086 не маскирует счет смены. Тем не менее, все остальные процессоры IA-32 (начиная с процессора Intel 286) маскируют число сдвигов до 5 бит, что приводит к максимальному числу 31. Это маскирование выполняется во всех режимах работы (включая режим virtual-8086) для уменьшить максимальное время выполнения инструкций.
1 http://codeyarns.com/2004/12/20/c-shift-operator-mayhem/
2 A7.8 Операторы сдвига, Приложение A. Справочное руководство, Язык программирования C
3 SAL / SAR / SHL / SHR - Shift, глава 4. Справочник по наборам инструкций, IA-32 Руководство разработчика программного обеспечения для архитектуры Intel
В C++ смещение четко определено, только если вы сдвигаете значение на несколько шагов меньше, чем размер типа. Если int
составляет 32 бита, то только от 0 до, включая 31 шаг, является четко определенным.
Итак, почему это?
Если вы посмотрите на базовое оборудование, которое выполняет сдвиг, если ему нужно только взглянуть на младшие пять битов значения (в 32-битном случае), оно может быть реализовано с использованием меньшего количества логических элементов, чем при проверке. каждый бит значения.
Ответ на вопрос в комментарии
C и C++ предназначены для максимально быстрой работы на любом доступном оборудовании. Сегодня сгенерированный код - это просто команда "shift" независимо от того, как базовое оборудование обрабатывает значения за пределами указанного диапазона. Если бы языки указывали, как должен вести себя сдвиг, сгенерированный мог бы проверить, что счетчик сдвига находится в диапазоне перед выполнением сдвига. Как правило, это даст три инструкции (сравнение, ветвление, сдвиг). (По общему признанию, в этом случае это не было бы необходимо, поскольку счет смены известен.)
Это неопределенное поведение в соответствии со стандартом C++:
Значение E1 << E2 - это E1 сдвинутые влево битовые позиции E2; освобожденные биты заполнены нулями. Если E1 имеет тип без знака, значение результата будет E1 × 2^E2, уменьшенное по модулю на единицу больше, чем максимальное значение, представляемое в типе результата. В противном случае, если E1 имеет тип со знаком и неотрицательное значение, а E1 × 2 ^ E2 представимо в типе результата, то это результирующее значение; в противном случае поведение не определено.
Ответы Lindydancer и 6502 объясняют, почему (на некоторых машинах) это происходит 1
это печатается (хотя поведение операции не определено). Я добавляю детали, если они не очевидны.
Я предполагаю, что (как и я) вы запускаете программу на процессоре Intel. GCC генерирует эти инструкции по сборке для сменной операции:
movl $32, %ecx
sall %cl, %eax
По теме sall
и другие операции смены, стр. 624 в Справочном руководстве по набору инструкций:
8086 не маскирует счет смены. Тем не менее, все остальные процессоры архитектуры Intel (начиная с процессора Intel 286) маскируют число сдвигов до пяти битов, что приводит к максимальному числу 31. Это маскирование выполняется во всех режимах работы (включая режим virtual-8086), чтобы уменьшить максимальное время выполнения инструкций.
Поскольку младшие 5 битов 32 равны нулю, то 1 << 32
эквивалентно 1 << 0
, который 1
,
Экспериментируя с большими числами, мы предсказали бы, что
cout << (a << 32) << " " << (a << 33) << " " << (a << 34) << "\n";
будет печатать 1 2 4
И это действительно то, что происходит на моей машине.
Это не работает, как ожидалось, потому что вы ожидаете слишком многого.
В случае x86 аппаратное обеспечение не заботится об операциях сдвига, где счетчик больше, чем размер регистра (см., Например, описание инструкции SHL в справочной документации по x86 для пояснения).
Стандарт C++ не хотел налагать дополнительные затраты, сообщая, что делать в этих случаях, потому что сгенерированный код был бы вынужден добавлять дополнительные проверки и логику для каждого параметрического сдвига.
С этой свободой разработчики компиляторов могут генерировать только одну инструкцию по сборке без какого-либо теста или ветвления.
Более "полезный" и "логический" подход был бы, например, иметь (x << y)
эквивалентно (x >> -y)
а также обработка больших счетчиков с логичным и последовательным поведением.
Однако это потребовало бы намного более медленной обработки для сдвига битов, поэтому выбор был делать то, что делает аппаратное обеспечение, оставляя программистам необходимость писать свои собственные функции для дополнительных случаев.
Учитывая, что разное оборудование в этих случаях делает разные вещи, стандарт говорит: "Что бы ни случилось, если вы делаете странные вещи, просто не обвиняйте C++, это ваша вина", переводится на юридический.
Сдвиг 32-битной переменной на 32 или более бит является неопределенным поведением и может привести к тому, что компилятор заставит демонов вылететь из вашего носа.
Серьезно, большую часть времени на выходе будет 0 (если int
32 бита или меньше), так как вы сдвигаете 1, пока он снова не упадет и ничего не останется, кроме 0. Но компилятор может оптимизировать его так, чтобы он делал что угодно.
Посмотрите отличную запись в блоге LLVM, что каждый программист C должен знать о неопределенном поведении, которую обязательно нужно прочитать каждому разработчику C.
Так как вы немного сдвигаете int на 32 бита; ты получишь: warning C4293: '<<' : shift count negative or too big, undefined behavior
в ВС Это означает, что вы выходите за пределы целого числа, и ответ может быть НИЧЕГО, потому что это неопределенное поведение.
Попробуйте использовать
1LL << 60
. Здесь
LL
для
long long
. Теперь вы можете перейти на максимум 61 бит.
У меня была такая же проблема, и это сработало для меня:
f = ((long long) 1 << (i-1));
Где я могу быть любое целое число больше 32 бит. 1 должно быть 64-битным целым числом, чтобы сдвиг работал.
Вы можете попробовать следующее. Это на самом деле дает вывод как 0
после 32
сдвиги влево.
#include<iostream>
#include<cstdio>
using namespace std;
int main()
{
int a = 1;
a <<= 31;
cout << (a <<= 1);
return 0;
}