Aritmética bitwise shift derecha "a shr b" con números enteros firmados que se almacenan en variables-resultados incorrectos! ¿El bug interno de Delphi?


Tengo una pregunta (o más probablemente un informe de error) sobre el comportamiento de cambio de bits en Delphi (probado en Borland Delphi 7).

Objetivo: realizar un desplazamiento "aritmético" a la derecha con cualquier número.

Esto significa que el bit de signo debe ser extendido: el número binario se llenará desde la izquierda con 1 en lugar de 0 si se estableció el bit más significativo de un número.

Entonces, el número " -1 "después de un desplazamiento aritmético a la derecha debe permanecer el mismo número (todos los bits = 1), pero con" lógico shift " (que siempre llena el número con ceros) debe dar un entero positivo máximo (máximo positivo firmado entero, para ser correcto)

Lo probé solo en el sistema de 32 bits (Windows); además, necesito que funcione explícitamente con enteros de 32 bits.

Parece que hay un error interno en Delphi con "shr" cuando el número de origen se almacena en una variable.

Mi código de ejemplo:

program bug;

{$APPTYPE CONSOLE}

var
I:Integer;
C:Cardinal;

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

(Es solo el principio). A continuación, vamos a probar algunos "shr" s:

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

"-1" es un equivalente firmado a "F FFFFFFFF". Parece que el comportamiento " shr " (aritmético o lógico) se basa en el hecho de si el número de origen está firmado o no (entero o cardinal).

La salida es:

0) -1
1) 2147483647

Bastante correcto. Entonces necesito tratar de convertir manualmente estos números en enteros o cardenales:

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

Resultado:

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

Todavía correcto. Por lo tanto, creo que puedo convertir cualquier cosa en "entero" si necesito un desplazamiento aritmético; o a "cardinal" cuando quiero un cambio lógico. Pero espera! Ejemplo con variables (declaradas arriba):

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

De repente:

6) 2147483647
7) 2147483647

INCORRECTO. Mi" I " era un entero con signo, y esperaba el cambio aritmético! Entonces, ¿tal vez el casting podría ayudar?

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

No, sigue siendo el mismo...

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

Las cosas son aún peores si intento hacer una función "a shr b" y la uso en su lugar:

// 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;

Ahora:

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

- Dejó de funcionar incluso con expresiones constantes: (tiene sentido, porque los números se almacenan de nuevo en variables dentro de una función)

C) 2147483647
D) 2147483647

Dado que necesito hacer mi desplazamiento aritmético correcto de todos modos, escribí estas fórmulas para hacer esto (desplazar "a" a la derecha por "b" bits). El primero es el cambio lógico:

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

Solo tengo que bitwise-y el resultado con "32 - b" unos (de la derecha) para borrar "b" bits de la izquierda en caso de que "shr" me falle e hizo desplazamiento aritmético en su lugar (ningún ejemplo muestra esto, pero solo para asegurarse). Entonces la aritmética shift:

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

Necesito bitwise-o el resultado con los "b" de la izquierda, pero solo cuando se estableció el bit más significativo; para hacer esto primero tomo el bit de signo con "(a shr 31) y 1", luego nego este número para obtener "-1" (o F FFFFFFFF – all bits =1) si la fuente era negativa, y 0 de lo contrario (pongo "0-x" en lugar de solo "-x" porque en mi puerto C en algunos casos bcc32 C-compiler reporta una advertencia de negar un entero sin signo); y finalmente lo cambié a "32-b" bits a la izquierda, así que tengo lo que deseo incluso cuando" shr " falla y dar ceros. Hice dos versiones de cada función para tratar con enteros y cardenales (también podría compartir nombres y "sobrecargarlos" para mí, pero aquí no haré esto para mantener el ejemplo claro):

// 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;

Pruébalo:

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

Y obtuvo resultados perfectos:

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

(El caso G sigue siendo correcto, porque "4294967295" es la versión sin signo de "-1")

Controles finales con variables:

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

Perfecto:

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

Para este error también traté de cambiar el segundo número (cantidad de desplazamiento) a una variable y/o probar diferentes casting – mismo error presente, parece que no está relacionado con el segundo argumento. Y tratar de convertir el resultado (a entero o cardinal) antes de la salida tampoco mejoró nada.

Para asegurarme de que no soy solo uno que tiene el error, traté de ejecutar todo mi ejemplo en http://codeforces.com / (hay un usuario registrado puede compilar y ejecutar una pieza de código en diferentes idiomas y compiladores en el lado del servidor) para ver la salida.

El compilador"Delphi 7" me dio exactamente lo que tengo – bug estaba presente. Opción alternativa, "Free Pascal 2" muestra aún más salida incorrecta:

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

Strange " 9223372036854775807 "en los casos 0-2-3 (había" -1"," Integer(-1) "y" Integer (F FFFFFFFF) " que no recuerdan).

Aquí está todo mi ejemplo en 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.

Entonces yo era curiosidades, es este error también presente en C++? Escribí un port a C++ y uso (Borland!) bcc32.exe para compilarlo.

Resultados:

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

Todo está bien. Aquí está la versión de C++, en caso de que alguien también quiera mirar:

#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

}

Antes de publicar aquí, traté de buscar este problema, y no encontré ninguna mención de este error. También miré aquí: ¿Cuál es el comportamiento de shl y shr para operandos de tamaño no registro? y aquí: Desplazamiento Aritmético a la Derecha en lugar que Logical Shift Right – pero se discutieron otros problemas (que el compilador internamente arroja cualquier tipo a un número de 32 bits antes de hacer shift real; o desplazar más de 31 bits), pero no el error mío.

Pero espera, aquí está mi problema: http://galfar.vevb.net/wp/2009/shift-right-delphi-vs-c/ !

Con una observación: dicen - {[24]]}

En Delphi el SHR es siempre una operación SHR: nunca toma en cuenta el signo.

Pero mi el ejemplo muestra que Delphi toma en cuenta el signo, pero solo cuando el número de origen es una expresión constante, no una variable. Así que "-10 shr 2" es igual a "-3", pero "x shr 2" es igual a "1073741821" cuando "x:=-10".

Así que creo que esto es un error, y no un "comportamiento" que "shr" siempre es lógico. Verás, no siempre.
Intentar habilitar / deshabilitar cualquier opción del compilador, como la comprobación de rango u optimizaciones, no cambió nada.

También, aquí he publicado ejemplos de cómo para evitar este problema y tener correcto desplazamiento aritmético. Y mi pregunta principal es: ¿tengo razón?

Parece que el desplazamiento a la izquierda siempre es bueno en Delphi (nunca usa el bit de signo original, y no "indefinido": para los enteros con signo se comporta como un casting a cardinal antes de cambiar y convertir el resultado en entero – un número puede convertirse repentinamente en negativo, por supuesto). Pero ahora me pregunto, ¿hay otros errores similares en Delphi? Este es el primer error realmente significativo que descubro en Delphi 7. Me encanta Delphi más que C++ exactamente porque siempre estaba seguro de que mi código está haciendo cada vez lo que quiero, sin depurar-probar cada nueva pieza inusual de código que estoy a punto de escribir (IMHO).

PD: Aquí hay algunos enlaces útiles que el sistema StackOverflow me sugiere cuando escribí mi título antes de publicar esta pregunta. Una vez más, información interesante, pero no sobre este error:

Desplazamiento de bits aritmético en un entero con signo
Mayús derecha firmado = resultado extraño?
Operadores de desplazamiento bit a bit en tipos con signo
¿Debe usar siempre ' int ' para los números en C, incluso si no son negativos?
¿Se definen los resultados de las operaciones bitwise en enteros con signo?
Verificando que C / C++ con signo de desplazamiento derecho es aritmética para un compilador en particular?
Emulando variable bit-shift usando solo cambios constantes?

P. P. S. Muchas gracias a Stack Exchange Equipo para asistencia en la publicación de este artículo. ¡Chicos, sois geniales!

Author: Community, 2015-08-24

3 answers

Hay un error, pero no es lo que piensas. Aquí está la documentación para shr:

Si x es un entero negativo, las operaciones shl y shr se hacen claras en el siguiente ejemplo:

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.

Así, shr y shl son siempre desplazamiento lógico y no son desplazamiento aritmético.

El defecto está en realidad en el manejo de constantes verdaderas negativas:

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

Aquí, -1 es un valor con signo. En realidad tiene el tipo Shortint, un bit 8 firmado entero. Pero los operadores de cambio operan en valores de 32 bits, por lo que es signo extendido a un valor de 32 bits. Así que eso significa que este extracto debe producir dos líneas con salida idéntica:

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

Y que la salida debe ser:

2147483647
2147483647

En las versiones modernas de Delphi, ciertamente de la versión 2010 y posteriores, pero posiblemente incluso versiones anteriores, ese es el caso.

Pero según su pregunta, en Delphi 7, -1 shr 1 evalúa a -1 lo cual es incorrecto, porque shr es lógico cambio.

Podemos adivinar la fuente del defecto. El compilador evalúa -1 shr 1 porque es un valor constante, y el compilador simplemente lo hace incorrectamente, usando desplazamiento aritmético en lugar de desplazamiento lógico.

Por cierto, la documentación contiene otro error. Dice:

Las operaciones x shl y y x shr y desplazan el valor de x a la izquierda o a la derecha por bits y, que (si x es un entero sin signo) es equivalente a multiplicar o dividir x por 2^y; el el resultado es del mismo tipo que x.

La parte final no es verdadera. La expresión x shl y es un tipo de 32 bits, si x es un tipo de 8, 16 o 32 bits, un tipo de 64 bits de lo contrario.


Dado que su objetivo real es implementar el cambio aritmético, entonces nada de esto le importa. No se puede usar shl o shr. Tendrás que implementar el cambio aritmético tú mismo. Sugiero que lo haga utilizando ensamblador en línea ya que sospecho que en última instancia podría ser más fácil de leer y verificar.

 18
Author: David Heffernan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-08-24 21:26:08

Si estás atascado con las versiones asm de los cambios aritméticos, aquí hay un código que funcionaría:

Tenga en cuenta que de acuerdo con: http://docwiki.embarcadero.com/RADStudio/XE8/en/Program_Control
Los primeros 3 parámetros se pasan en los registros de la siguiente manera: EAX, EDX, ECX

En 64 bits el orden de registro es: RCX, RDX, R8, R9

El resultado de las funciones se pasa en 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.
 3
Author: Johan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-08-26 19:51:01

Probé en Delphi 7 y parece que simplemente usar "div 2" en una variable entera compila directamente a una operación de ensamblador SAR (como se ve en la ventana de CPU).

Actualización: Div no funciona correctamente como reemplazo de SAR como expliqué en mi comentario en esta respuesta. El compilador genera una instrucción SAR, pero luego prueba el bit de signo y ajusta la respuesta agregando el bit que se desplazó a la derecha si el bit de signo está establecido. Esto da el comportamiento correcto para el div operador en números negativos, pero derrota nuestro objetivo de obtener el comportamiento correcto SAR.

 1
Author: Jannie Gerber,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-10-17 15:06:16