使用存储在变量中的有符号整数进行算术位右移 "a shr b"——错误的结果!内部 Delphi 的错误?

Arithmetic bitwise shift right "a shr b" with signed integers that stored in variables – wrong results! Internal Delphi’s bug?

我有一个关于 Delphi(在 Borland Delphi 7 中测试)中的移位行为的问题(或者更可能是错误报告)。

目标:对任意数字执行 "Arithmetic" 位右移。

这意味着 sign-bit 必须扩展 – 如果设置了数字的最高有效位,二进制数将从左边填充 1 而不是 0。

因此,算术右移后的数字“-1”必须保持相同的数字(所有位 = 1),但是 "logical shift"(总是用零填充数字)必须给出最大正数整数(最大正有符号整数,正确)

我只在32位系统上测试过(Windows);此外,我需要它明确地使用 32 位整数。

当源编号存储在变量中时,Delphi 和 "shr" 似乎存在内部错误。

我的示例代码:

program bug;

{$APPTYPE CONSOLE}

var
I:Integer;
C:Cardinal;

begin
I := -1;  // we’ll need that later
C := $FFFFFFFF;

(这只是开始)。接下来,让我们尝试一些 "shr"s:

Writeln('0) ', -1 shr 1 );
Writeln('1) ', $FFFFFFFF shr 1 );

"-1" 是等价于 "$FFFFFFFF" 的符号。似乎 "shr" 行为(算术或逻辑)是基于源数字是否有符号(整数或基数)这一事实。

输出为:

0) -1
1) 2147483647

非常正确。然后我需要尝试手动将这些数字转换为整数或基数:

Writeln('2) ', Integer(-1) shr 1 );
Writeln('3) ', Integer($FFFFFFFF) shr 1 );
Writeln('4) ', Cardinal(-1) shr 1 );
Writeln('5) ', Cardinal($FFFFFFFF) shr 1 );

结果:

2) -1
3) -1
4) 2147483647
5) 2147483647

仍然正确。所以,我认为如果我需要算术移位,我可以将任何东西转换为 "integer";或者在我想要逻辑移位时转换为 "cardinal" 。可是等等!变量示例(上面声明):

Writeln('6) ', I shr 1 );
Writeln('7) ', C shr 1 );

突然:

6) 2147483647
7) 2147483647

不正确。我的 "I" 是一个带符号的整数,我期待算术移位!那么,也许铸造可以帮助?

Writeln('8) ', Integer(I) shr 1 );
Writeln('9) ', Cardinal(I) shr 1 );
Writeln('A) ', Integer(C) shr 1 );
Writeln('B) ', Cardinal(C) shr 1 );

没有,还是一样...

8) 2147483647
9) 2147483647
A) 2147483647
B) 2147483647

如果我尝试创建一个函数 "a shr b" 并改用它,情况会更糟:

// Simple shift right with signed integers
function shrI(a,b:Integer):Integer;
begin
Result := a shr b;
end;

// Simple shift right with unsigned integers
function shrC(a,b:Cardinal):Cardinal;
begin
Result := a shr b;
end;

现在:

Writeln('C) ', shrI(-1,1) );
Writeln('D) ', shrC($FFFFFFFF,1) );

– 即使使用常量表达式它也停止工作:(这是有道理的,因为数字再次存储在函数内的变量中)

C) 2147483647
D) 2147483647

由于无论如何我都需要进行正确的算术移位,因此我编写了这些公式来执行此操作(将 "a" 右移 "b" 位)。首先是逻辑移位:

(a shr b) and ((1 shl (32-b))-1)

我只需要 bitwise-and 带有“32 - b”的结果(从右边开始)以清除 "b" 左边的位以防 "shr" 让我失败并进行算术移位相反(没有示例显示这一点,但只是为了确保)。然后算术移位:

(a shr b) or (( 0-((a shr 31) and 1)) shl (32-b))

我需要 bitwise-or 左边的 "b" 个结果,但只有当设置了最高有效位时;为此,我首先使用“(a shr 31) 和 1”取符号位,然后如果源为负则取负数以获得“-1”(或 $FFFFFFFF – 所有位 =1),否则为 0(我把“0-x”而不仅仅是“-x”,因为在我的 C-port 中,在某些情况下,bcc32 C-compiler 报告了一个警告,即要否定一个无符号整数);最后我把它移到左边的“32 - b”位,所以即使 "shr" 失败并给出零,我也得到了我想要的。我为每个函数制作了两个版本来处理整数和基数(我也可以为我共享名称和 "overload" 它们,但在这里我不会这样做以保持示例清晰):

// Logical shift right with signed integers
function srlI(a,b:Integer):Integer;
begin
Result := (a shr b) and ((1 shl (32-b))-1);
end;

// Arithmetic shift right with signed integers
function sraI(a,b:Integer):Integer;
begin
Result := (a shr b) or (( 0-((a shr 31) and 1)) shl (32-b));
end;

// Logical shift right with unsigned integers
function srlC(a,b:Cardinal):Cardinal;
begin
Result := (a shr b) and ((1 shl (32-b))-1);
end;

// Arithmetic shift right with unsigned integers
function sraC(a,b:Cardinal):Cardinal;
begin
Result := (a shr b) or (( 0-((a shr 31) and 1)) shl (32-b));
end;

测试一下:

Writeln('E) ', sraI(-1,1) );
Writeln('F) ', srlI(-1,1) );
Writeln('G) ', sraC($FFFFFFFF,1) );
Writeln('H) ', srlC($FFFFFFFF,1) );

并获得了完美的结果:

E) -1
F) 2147483647
G) 4294967295
H) 2147483647

(G-case 仍然正确,因为“4294967295”是“-1”的无符号版本)

最终检查变量:

Writeln('K) ', sraI(I,1) );
Writeln('L) ', srlI(I,1) );
Writeln('M) ', sraC(C,1) );
Writeln('N) ', srlC(C,1) );

完美:

K) -1
L) 2147483647
M) 4294967295
N) 2147483647

对于这个错误,我还尝试将第二个数字(移位量)更改为变量 and/or 尝试不同的转换 - 存在相同的错误,看起来它与第二个参数无关。并且在输出之前尝试转换结果(整数或基数)也没有任何改善。

为了确保不仅仅是我有这个错误,我尝试 运行 我在 http://codeforces.com/ 的整个示例(注册用户可以编译和执行一段代码在 server-side) 上使用不同的语言和编译器查看输出。

"Delphi 7" 编译器给了我我所拥有的——存在错误。替代选项,"Free Pascal 2" 显示更多错误输出:

0) 9223372036854775807
1) 2147483647
2) 9223372036854775807
3) 9223372036854775807
4) 2147483647
5) 2147483647
6) 2147483647
7) 2147483647
8) 2147483647
9) 2147483647
A) 2147483647
B) 2147483647
C) 2147483647
D) 2147483647
E) -1
F) 2147483647
G) 4294967295
H) 2147483647
K) -1
L) 2147483647
M) 4294967295
N) 2147483647

奇怪的“9223372036854775807”在情况 0-2-3 中(有“-1”,"Integer(-1)" 和 "Integer($FFFFFFFF)" 谁不记得了)。

这是我在 Delphi 中的整个示例:

program bug;

{$APPTYPE CONSOLE}

// Simple shift right with signed integers
function shrI(a,b:Integer):Integer;
begin
Result := a shr b;
end;

// Simple shift right with unsigned integers
function shrC(a,b:Cardinal):Cardinal;
begin
Result := a shr b;
end;

// Logical shift right with signed integers
function srlI(a,b:Integer):Integer;
begin
Result := (a shr b) and ((1 shl (32-b))-1);
end;

// Arithmetic shift right with signed integers
function sraI(a,b:Integer):Integer;
begin
Result := (a shr b) or (( 0-((a shr 31) and 1)) shl (32-b));
end;

// Logical shift right with unsigned integers
function srlC(a,b:Cardinal):Cardinal;
begin
Result := (a shr b) and ((1 shl (32-b))-1);
end;

// Arithmetic shift right with unsigned integers
function sraC(a,b:Cardinal):Cardinal;
begin
Result := (a shr b) or (( 0-((a shr 31) and 1)) shl (32-b));
end;

var
I:Integer;
C:Cardinal;

begin
I := -1;
C := $FFFFFFFF;

Writeln('0) ', -1 shr 1 );
Writeln('1) ', $FFFFFFFF shr 1 );
// 0) -1           - correct
// 1) 2147483647   - correct

Writeln('2) ', Integer(-1) shr 1 );
Writeln('3) ', Integer($FFFFFFFF) shr 1 );
// 2) -1           - correct
// 3) -1           - correct

Writeln('4) ', Cardinal(-1) shr 1 );
Writeln('5) ', Cardinal($FFFFFFFF) shr 1 );
// 4) 2147483647   - correct
// 5) 2147483647   - correct

Writeln('6) ', I shr 1 );
Writeln('7) ', C shr 1 );
// 6) 2147483647   - INCORRECT!
// 7) 2147483647   - correct

Writeln('8) ', Integer(I) shr 1 );
Writeln('9) ', Cardinal(I) shr 1 );
// 8) 2147483647   - INCORRECT!
// 9) 2147483647   - correct

Writeln('A) ', Integer(C) shr 1 );
Writeln('B) ', Cardinal(C) shr 1 );
// A) 2147483647   - INCORRECT!
// B) 2147483647   - correct

Writeln('C) ', shrI(-1,1) );
Writeln('D) ', shrC($FFFFFFFF,1) );
// C) 2147483647   - INCORRECT!
// D) 2147483647   - correct

Writeln('E) ', sraI(-1,1) );
Writeln('F) ', srlI(-1,1) );
// E) -1           - correct
// F) 2147483647   - correct

Writeln('G) ', sraC($FFFFFFFF,1) );
Writeln('H) ', srlC($FFFFFFFF,1) );
// G) 4294967295   - correct
// H) 2147483647   - correct

Writeln('K) ', sraI(I,1) );
Writeln('L) ', srlI(I,1) );
// K) -1           - correct
// L) 2147483647   - correct

Writeln('M) ', sraC(C,1) );
Writeln('N) ', srlC(C,1) );
// M) 4294967295   - correct
// N) 2147483647   - correct

end.

然后我很好奇,这个错误是否也存在于 C++ 中?我写了一个 C++ 端口并使用 (Borland!) bcc32.exe 编译它。

结果:

0) -1
1) 2147483647
2) -1
3) -1
4) 2147483647
5) 2147483647
6) -1
7) 2147483647
8) -1
9) 2147483647
A) -1
B) 2147483647
C) -1
D) 2147483647
E) -1
F) 2147483647
G) 4294967295
H) 2147483647
K) -1
L) 2147483647
M) 4294967295
N) 2147483647

一切正常。这里是C++版本,万一有人也想看:

#include <iostream>
using namespace std;

// Simple shift right with signed integers
int shrI(int a, int b){
return a >> b;
}

// Simple shift right with unsigned integers
unsigned int shrC(unsigned int a, unsigned int b){
return a >> b;
}

// Logical shift right with signed integers
int srlI(int a, int b){
return (a >> b) & ((1 << (32-b))-1);
}

// Arithmetic shift right with signed integers
int sraI(int a, int b){
return (a >> b) | (( 0-((a >> 31) & 1)) << (32-b));
}

// Logical shift right with unsigned integers
unsigned int srlC(unsigned int a, unsigned int b){
return (a >> b) & ((1 << (32-b))-1);
}

// Arithmetic shift right with unsigned integers
unsigned int sraC(unsigned int a, unsigned int b){
return (a >> b) | (( 0-((a >> 31) & 1)) << (32-b));
}

int I;
unsigned int C;

int main(){
I = -1;
C = 0xFFFFFFFF;

cout<<"0) "<<( -1 >> 1 )<<endl;
cout<<"1) "<<( 0xFFFFFFFF >> 1 )<<endl;
// 0) -1           - correct
// 1) 2147483647   - correct

cout<<"2) "<<( ((int)(-1)) >> 1 )<<endl;
cout<<"3) "<<( ((int)(0xFFFFFFFF)) >> 1 )<<endl;
// 2) -1           - correct
// 3) -1           - correct

cout<<"4) "<<( ((unsigned int)(-1)) >> 1 )<<endl;
cout<<"5) "<<( ((unsigned int)(0xFFFFFFFF)) >> 1 )<<endl;
// 4) 2147483647   - correct
// 5) 2147483647   - correct

cout<<"6) "<<( I >> 1 )<<endl;
cout<<"7) "<<( C >> 1 )<<endl;
// 6) -1           - correct
// 7) 2147483647   - correct

cout<<"8) "<<( ((int)(I)) >> 1 )<<endl;
cout<<"9) "<<( ((unsigned int)(I)) >> 1 )<<endl;
// 8) -1           - correct
// 9) 2147483647   - correct

cout<<"A) "<<( ((int)(C)) >> 1 )<<endl;
cout<<"B) "<<( ((unsigned int)(C)) >> 1 )<<endl;
// A) -1           - correct
// B) 2147483647   - correct

cout<<"C) "<<( shrI(-1,1) )<<endl;
cout<<"D) "<<( shrC(0xFFFFFFFF,1) )<<endl;
// C) -1           - correct
// D) 2147483647   - correct

cout<<"E) "<<( sraI(-1,1) )<<endl;
cout<<"F) "<<( srlI(-1,1) )<<endl;
// E) -1           - correct
// F) 2147483647   - correct

cout<<"G) "<<( sraC(0xFFFFFFFF,1) )<<endl;
cout<<"H) "<<( srlC(0xFFFFFFFF,1) )<<endl;
// G) 4294967295   - correct
// H) 2147483647   - correct

cout<<"K) "<<( sraI(I,1) )<<endl;
cout<<"L) "<<( srlI(I,1) )<<endl;
// K) -1           - correct
// L) 2147483647   - correct

cout<<"M) "<<( sraC(C,1) )<<endl;
cout<<"N) "<<( srlC(C,1) )<<endl;
// M) 4294967295   - correct
// N) 2147483647   - correct

}

在这里发帖之前,我试着搜索这个问题,但没有找到任何关于这个错误的信息。我还看了这里:What is the behaviour of shl and shr for non register sized operands? and here: Arithmetic Shift Right rather than Logical Shift Right – 但讨论了其他问题(编译器在进行实际移位之前在内部将任何类型强制转换为 32 位数字;或移位超过 31 位),但不是我的错误。

等等,这是我的问题:http://galfar.vevb.net/wp/2009/shift-right-delphi-vs-c/

一句话:他们说–

In Delphi the SHR is always a SHR operation: it never takes into account the sign.

但我的示例显示 Delphi 确实 考虑了符号,但仅当源编号是常量表达式而不是变量时。所以“-10 shr 2”等于“-3”,但是当"x:=-10".

时"x shr 2"等于“1073741821”

所以我认为这是一个错误,而不是 "behavior" "shr" 总是合乎逻辑的。你看,并不总是。
尝试 enable/disable 任何编译器选项,例如范围检查或优化都没有改变任何东西。

此外,我在这里发布了如何绕过这个问题并正确进行算术移位的示例。我的主要问题是:我说得对吗?

似乎左移在 Delphi 中总是好的(它从不使用原始符号位,而不是 "undefined":对于有符号整数,它在移位和转换结果之前表现为转换为基数回到整数——一个数字当然可以突然变成负数)。但是现在我想知道,Delphi中是否还有其他类似的错误?这是我在 Delphi 7 中发现的第一个真正重要的错误。我爱 Delphi 胜过 C++,因为我始终确信我的代码每次都在做我想做的事情,而不是 debug-testing我将要编写的每一段新的不寻常的代码(恕我直言)。

P.S。这里有一些有用的链接,当我在发布这个问题之前输入我的标题时,Whosebug 系统会建议我。同样,有趣的信息,但与此错误无关:

Arithmetic bit-shift on a signed integer
Signed right shift = strange result?
Bitwise shift operators on signed types
Should you always use 'int' for numbers in C, even if they are non-negative?
Are the results of bitwise operations on signed integers defined?
Verifying that C / C++ signed right shift is arithmetic for a particular compiler?
Emulating variable bit-shift using only constant shifts?

P.P.S。非常感谢 Stack Exchange 团队在发布本文时提供的帮助。伙计们,你们太棒了!

有一个错误,但不是你想的那样。这是 shrdocumentation:

If x is a negative integer, the shl and shr operations are made clear in the following example:

var
  x: integer;
  y: string;

...
begin
  x := -20;
  x := x shr 1;
  //As the number is shifted to the right by 1 bit, the sign bit's value replaced is
  //with 0 (all negative numbers have the sign bit set to 1). 

  y := IntToHex(x, 8);
  writeln(y);
  //Therefore, x is positive.
  //Decimal value: 2147483638
  //Hexadecimal value: 7FFFFFF6
  //Binary value: 0111 1111 1111 1111 1111 1111 1111 0110
end.

因此,shrshl总是逻辑移位而不是算术移位。

缺陷实际上是在负真常量的处理上:

Writeln('0) ', -1 shr 1 );

这里,-1是一个带符号的值。它实际上具有类型 Shortint,一个带符号的 8 位整数。但是移位运算符对 32 位值进行操作,因此它被符号扩展为 32 位值。所以这意味着这段摘录应该产生两行具有相同的输出:

var
  i: Integer;
....
i := -1;
Writeln(-1 shr 1);
Writeln( i shr 1);

并且输出应该是:

2147483647
2147483647

在 Delphi 的现代版本中,肯定是 2010 版及更高版本,但可能更早的版本也是如此。

但是根据你的问题,在 Delphi 7 中,-1 shr 1 计算为 -1 这是错误的,因为 shr 是逻辑移位。

我们可以猜测缺陷的来源。编译器评估 -1 shr 1 因为它是一个常量值,而编译器只是错误地使用算术移位而不是逻辑移位。

顺便说一句,文档中包含另一个错误。它说:

The operations x shl y and x shr y shift the value of x to the left or right by y bits, which (if x is an unsigned integer) is equivalent to multiplying or dividing x by 2^y; the result is of the same type as x.

最后一部分不正确。表达式 x shl y 是 32 位类型,如果 x 是 8、16 或 32 位类型,否则是 64 位类型。


由于您的实际目标是实现算术移位,因此 none 这对您很重要。您不能使用 shlshr。您将不得不自己实施算术移位。我建议您使用内联汇编器这样做,因为我怀疑这最终可能更容易阅读和验证。

如果您受困于算术移位的 asm 版本,这里有一些可行的代码:

注意根据:http://docwiki.embarcadero.com/RADStudio/XE8/en/Program_Control
前 3 个参数在寄存器中传递如下: EAX、EDX、ECX

在 64 位中,寄存器顺序是: RCX、RDX、R8、R9

函数的结果在 EAX 中传递

unit SARL;

interface

  function sar(const base: integer; shift: byte): integer; 
  function sal(const base: integer; shift: byte): integer;

implementation

  function sar(const base: integer; shift: byte): integer;
  asm
    {$IFDEF CPU64BIT}
    mov eax,ecx
    mov ecx,edx
    sar eax,cl
    {$ELSE}
    mov ecx,edx
    sar eax,cl  //shr is very different from sar
    {$ENDIF}
  end;

  function sal(const base: integer; shift: byte): integer; 
  asm
    {$IFDEF CPU64BIT}
    mov eax,ecx
    mov ecx,edx
    shl eax,cl
    {$ELSE}
    mov ecx,edx
    shl eax,cl   //Note that sal and shl are the same thing.
    {$ENDIF}
  end;

end.

我在 Delphi 7 中进行了测试,似乎只需对整数变量使用 "div 2" 即可直接编译为 SAR 汇编程序操作(如 CPU window).

更新:Div 无法正常替代 SAR,正如我在此回答中的评论中所解释的那样。编译器生成 SAR 语句,然后测试符号位并通过添加在符号位已设置时向右移出的位来调整答案。这为 div 运算符提供了对负数的正确行为,但违背了我们获得正确 SAR 行为的目标。