Каково поведение shl и shr для операндов без регистрового размера?
Этот вопрос вдохновлен моими попытками ответить на другой вопрос: Преобразование десятичного/целочисленного в двоичный - как и почему он работает так, как он делает?
Только документация для операторов побитового сдвига, которые я могу найти, говорит:
Операции x shl y и x shr y сдвигают значение x влево или вправо на y битов, которое (если x является целым без знака) эквивалентно умножению или делению x на 2 ^ y; результат имеет тот же тип, что и x. Например, если N хранит значение 01101 (десятичное число 13), тогда N shl 1 возвращает 11010 (десятичное значение 26). Обратите внимание, что значение y интерпретируется по модулю размера типа x. Так, например, если x является целым числом, x shl 40 интерпретируется как x shl 8, потому что целое число равно 32 битам, а 40 mod 32 равно 8.
Рассмотрим эту программу:
{$APPTYPE CONSOLE}
program BitwiseShift;
var
u8: Byte;
u16: Word;
u32: LongWord;
u64: UInt64;
begin
u8 := $ff;
Writeln((u8 shl 7) shr 7);
// expects: 1 actual: 255
u16 := $ffff;
Writeln((u16 shl 15) shr 15);
// expects: 1 actual: 65535
u32 := $ffffffff;
Writeln((u32 shl 31) shr 31);
// expects: 1 actual: 1
u64 := $ffffffffffffffff;
Writeln((u64 shl 63) shr 63);
// expects: 1 actual: 1
end.
Я запустил это как с XE3, так и с XE5, как для 32-разрядных, так и для 64-битных компиляторов Windows, а исходящие из них непротиворечивы, как указано в коде выше.
Я ожидал, что (u8 shl 7) shr 7
будет полностью оцениваться в контексте 8-битного типа. Поэтому, когда биты сдвинуты за конец этого 8-битного типа, эти биты теряются.
Мой вопрос в том, почему программа ведет себя так же, как и она.
Интересно, что я перевел программу на С++, и на моем 64-битном средстве 4.6.3 был получен тот же результат.
#include <cstdint>
#include <iostream>
int main()
{
uint8_t u8 = 0xff;
std::cout << ((u8 << 7) >> 7) << std::endl;
uint16_t u16 = 0xffff;
std::cout << ((u16 << 15) >> 15) << std::endl;
uint32_t u32 = 0xffffffff;
std::cout << ((u32 << 31) >> 31) << std::endl;
uint64_t u64 = 0xffffffffffffffff;
std::cout << ((u64 << 63) >> 63) << std::endl;
}
Ответы
Ответ 1
Причина тип продвижения:
Одним из особых случаев неявного преобразования типов является продвижение по типу, где компилятор автоматически расширяет двоичное представление объекты целых или с плавающей точкой. Акции обычно используется с типами, меньшими, чем собственный тип целевой платформы ALU перед арифметическими и логическими операциями, чтобы сделать такие операции или более эффективны, если ALU может работать с более чем один тип. C и С++ выполняют такую рекламу для объектов boolean, character, wide character, enumeration и short integer типы, которые продвигаются до int, и для объектов типа float, которые удваиваются. В отличие от некоторых других типов конверсий, рекламных акций никогда не теряйте точность или не изменяйте значение, хранящееся в объекте.
Итак, в следующем коде
var
u8: Byte;
begin
u8 := $ff;
Writeln((u8 shl 7) shr 7);
..
значение u8
повышается до 32-значных значений до shl
; для исправления результата вам потребуется явно преобразование типов:
Writeln(Byte(u8 shl 7) shr 7);
Стандарт С++, раздел 4.5 Интегральные рекламные акции:
Значение типа char, подписанное char, unsigned char, short int или unsigned short int может быть преобразован в rvalue типа int, если int может представлять все значения типа источника; в противном случае значение r источника может быть преобразуется в rvalue типа unsigned int.
Чтобы проверить, соответствует ли Delphi одно и то же соглашение в продвижении типа, я написал следующее приложение:
var
u8: Byte;
u16: Word;
u32: LongWord;
procedure Test(Value: Integer); overload;
begin
Writeln('Integer');
end;
procedure Test(Value: Cardinal); overload;
begin
Writeln('Cardinal');
end;
begin
u8 := $ff;
Test(u8); // 'Integer'
u16 := $ffff;
Test(u16); // 'Integer'
u32 := $ffffffff;
Test(u32); // 'Cardinal'
Readln;
end.
Итак, я считаю, что здесь не должно быть разницы между Delphi и С++.
Ответ 2
Я изменил ваш тест на
procedure TestByte;
var
u8 : Byte;
LShift : Integer;
begin
Writeln( 'Byte' );
u8 := $FF;
LShift := 7;
Writeln( IntToHex( u8, 16 ), '-', LShift : 2, ' ', IntToHex( u8 shl LShift, 16 ), ' ', IntToHex( ( u8 shl LShift ) shr LShift, 16 ) );
LShift := 15;
Writeln( IntToHex( u8, 16 ), '-', LShift : 2, ' ', IntToHex( u8 shl LShift, 16 ), ' ', IntToHex( ( u8 shl LShift ) shr LShift, 16 ) );
LShift := 31;
Writeln( IntToHex( u8, 16 ), '-', LShift : 2, ' ', IntToHex( u8 shl LShift, 16 ), ' ', IntToHex( ( u8 shl LShift ) shr LShift, 16 ) );
LShift := 63;
Writeln( IntToHex( u8, 16 ), '-', LShift : 2, ' ', IntToHex( u8 shl LShift, 16 ), ' ', IntToHex( ( u8 shl LShift ) shr LShift, 16 ) );
end;
procedure TestWord;
var
u8 : Word;
LShift : Integer;
begin
Writeln( 'Word' );
u8 := $FF;
LShift := 7;
Writeln( IntToHex( u8, 16 ), '-', LShift : 2, ' ', IntToHex( u8 shl LShift, 16 ), ' ', IntToHex( ( u8 shl LShift ) shr LShift, 16 ) );
LShift := 15;
Writeln( IntToHex( u8, 16 ), '-', LShift : 2, ' ', IntToHex( u8 shl LShift, 16 ), ' ', IntToHex( ( u8 shl LShift ) shr LShift, 16 ) );
LShift := 31;
Writeln( IntToHex( u8, 16 ), '-', LShift : 2, ' ', IntToHex( u8 shl LShift, 16 ), ' ', IntToHex( ( u8 shl LShift ) shr LShift, 16 ) );
LShift := 63;
Writeln( IntToHex( u8, 16 ), '-', LShift : 2, ' ', IntToHex( u8 shl LShift, 16 ), ' ', IntToHex( ( u8 shl LShift ) shr LShift, 16 ) );
end;
procedure TestLongWord;
var
u8 : LongWord;
LShift : Integer;
begin
Writeln( 'LongWord' );
u8 := $FF;
LShift := 7;
Writeln( IntToHex( u8, 16 ), '-', LShift : 2, ' ', IntToHex( u8 shl LShift, 16 ), ' ', IntToHex( ( u8 shl LShift ) shr LShift, 16 ) );
LShift := 15;
Writeln( IntToHex( u8, 16 ), '-', LShift : 2, ' ', IntToHex( u8 shl LShift, 16 ), ' ', IntToHex( ( u8 shl LShift ) shr LShift, 16 ) );
LShift := 31;
Writeln( IntToHex( u8, 16 ), '-', LShift : 2, ' ', IntToHex( u8 shl LShift, 16 ), ' ', IntToHex( ( u8 shl LShift ) shr LShift, 16 ) );
LShift := 63;
Writeln( IntToHex( u8, 16 ), '-', LShift : 2, ' ', IntToHex( u8 shl LShift, 16 ), ' ', IntToHex( ( u8 shl LShift ) shr LShift, 16 ) );
end;
procedure TestUInt64;
var
u8 : UInt64;
LShift : Integer;
begin
Writeln( 'UInt64' );
u8 := $FF;
LShift := 7;
Writeln( IntToHex( u8, 16 ), '-', LShift : 2, ' ', IntToHex( u8 shl LShift, 16 ), ' ', IntToHex( ( u8 shl LShift ) shr LShift, 16 ) );
LShift := 15;
Writeln( IntToHex( u8, 16 ), '-', LShift : 2, ' ', IntToHex( u8 shl LShift, 16 ), ' ', IntToHex( ( u8 shl LShift ) shr LShift, 16 ) );
LShift := 31;
Writeln( IntToHex( u8, 16 ), '-', LShift : 2, ' ', IntToHex( u8 shl LShift, 16 ), ' ', IntToHex( ( u8 shl LShift ) shr LShift, 16 ) );
LShift := 63;
Writeln( IntToHex( u8, 16 ), '-', LShift : 2, ' ', IntToHex( u8 shl LShift, 16 ), ' ', IntToHex( ( u8 shl LShift ) shr LShift, 16 ) );
end;
begin
TestByte;
TestWord;
TestLongWord;
TestUInt64;
end.
и он дал мне этот результат
Byte
00000000000000FF- 7 0000000000007F80 00000000000000FF
00000000000000FF-15 00000000007F8000 00000000000000FF
00000000000000FF-31 0000000080000000 0000000000000001
00000000000000FF-63 0000000080000000 0000000000000001
Word
00000000000000FF- 7 0000000000007F80 00000000000000FF
00000000000000FF-15 00000000007F8000 00000000000000FF
00000000000000FF-31 0000000080000000 0000000000000001
00000000000000FF-63 0000000080000000 0000000000000001
LongWord
00000000000000FF- 7 0000000000007F80 00000000000000FF
00000000000000FF-15 00000000007F8000 00000000000000FF
00000000000000FF-31 0000000080000000 0000000000000001
00000000000000FF-63 0000000080000000 0000000000000001
UInt64
00000000000000FF- 7 0000000000007F80 00000000000000FF
00000000000000FF-15 00000000007F8000 00000000000000FF
00000000000000FF-31 0000007F80000000 00000000000000FF
00000000000000FF-63 8000000000000000 0000000000000001
Таким образом, внутренние значения не обрабатываются в объявленном им типе
Ответ 3
То, что происходит за кулисами, на самом деле довольно интересно.
Учитывая следующее приложение Delphi:
program BitwiseShift;
var
u8: Byte;
begin
//all in one go
u8 := $ff;
Writeln((u8 shl 7) shr 7);
// expects: 1 actual: 255
//step by step
u8 := $ff;
u8:= u8 shl 7;
u8:= u8 shr 7;
WriteLn(u8);
// expects: 1 actual: 1
end.
Создается следующая сборка (в XE2)
BitwiseShift.dpr.10: Writeln((u8 shl 7) shr 7);
004060D3 33D2 xor edx,edx
004060D5 8A1594AB4000 mov dl,[$0040ab94]
004060DB C1E207 shl edx,$07
004060DE C1EA07 shr edx,$07
004060E1 A114784000 mov eax,[$00407814] <<--- The result is NOT a byte!!
004060E6 E895D6FFFF call @Write0Long
004060EB E864D9FFFF call @WriteLn
004060F0 E8A7CCFFFF call @_IOTest
BitwiseShift.dpr.13: u8 := $ff;
004060F5 C60594AB4000FF mov byte ptr [$0040ab94],$ff
BitwiseShift.dpr.14: u8:= u8 shl 7;
004060FC C02594AB400007 shl byte ptr [$0040ab94],$07
BitwiseShift.dpr.15: u8:= u8 shr 7;
00406103 33C0 xor eax,eax
00406105 A094AB4000 mov al,[$0040ab94]
0040610A C1E807 shr eax,$07
0040610D A294AB4000 mov [$0040ab94],al
BitwiseShift.dpr.16: WriteLn(u8);
00406112 33D2 xor edx,edx
00406114 8A1594AB4000 mov dl,[$0040ab94]
0040611A A114784000 mov eax,[$00407814]
0040611F E85CD6FFFF call @Write0Long
00406124 E82BD9FFFF call @WriteLn
00406129 E86ECCFFFF call @_IOTest
Правило, насколько я могу разобраться, это:
Правило
Узость выполняемого сдвига (8/16/32 бит) зависит от размер результата сдвига, а не размер переменных используется в смену. В исходном случае вы не резервируете переменную чтобы сохранить результат, и, таким образом, Delphi выбирает значение по умолчанию (целое число) для вы.
Как получить ожидаемый результат
В моем измененном случае результат равен размеру байта, и, следовательно, данные обрушиваются на этот размер.
Если вы измените свой случай, чтобы принудительно использовать байты, ваши первоначальные ожидания будут выполнены:
Writeln(byte(byte(u8 shl 7) shr 7));
// expects: 1 actual: 1
Project24.dpr.19: Writeln(byte(byte(u8 shl 7) shr 7));
00406135 8A1594AB4000 mov dl,[$0040ab94]
0040613B C1E207 shl edx,$07
0040613E 81E2FF000000 and edx,$000000ff
00406144 C1EA07 shr edx,$07
00406147 81E2FF000000 and edx,$000000ff
0040614D A114784000 mov eax,[$00407814]
00406152 E829D6FFFF call @Write0Long
00406157 E8F8D8FFFF call @WriteLn
0040615C E83BCCFFFF call @_IOTest