Подтвердить что ты не робот

Каково поведение 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;
}
4b9b3361

Ответ 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