c++ - ¿Qué hace i+=(i &-i)? ¿Es portátil?
integer bit-manipulation (6)
Sea i un tipo entero con signo. Considerar
i += (i&-i);
i -= (i&-i);
donde inicialmente i>0 .
- ¿Qué hacen estos? ¿Hay un código equivalente usando aritmética solamente?
- ¿Depende esto de una representación específica de bits de enteros negativos?
Fuente: código de configuración de un rompecabezas de codificación en línea (sin ninguna explicación / comentarios).
Aquí está lo que busqué impulsado por otras respuestas. Las manipulaciones de bits
i -= (i&-i); // strips off the LSB (least-significant bit)
i += (i&-i); // adds the LSB
Se utilizan, predominantemente, para atravesar un árbol de Fenwick . En particular, i&-i proporciona el LSB si los enteros con signo se representan mediante el complemento de dos . Como ya lo señaló Peter Fenwick en su propuesta original, esto no es portátil a otras representaciones de enteros con signo. Sin embargo,
i &= i-1; // strips off the LSB
es (también funciona con el complemento y las representaciones de magnitudes firmadas ) y tiene una operación menos.
Sin embargo, no parece haber una alternativa portátil simple para agregar el LSB.
En una arquitectura de complemento de dos, con enteros con signo de 4 bits:
| i | bin | comp | -i | i&-i | dec |
+----+------+------+----+------+-----+
| 0 | 0000 | 0000 | -0 | 0000 | 0 |
| 1 | 0001 | 1111 | -1 | 0001 | 1 |
| 2 | 0010 | 1110 | -2 | 0010 | 2 |
| 3 | 0011 | 1101 | -3 | 0001 | 1 |
| 4 | 0100 | 1100 | -4 | 0100 | 4 |
| 5 | 0101 | 1011 | -5 | 0001 | 1 |
| 6 | 0110 | 1010 | -6 | 0010 | 2 |
| 7 | 0111 | 1001 | -7 | 0001 | 1 |
| -8 | 1000 | 1000 | -8 | 1000 | 8 |
| -7 | 1001 | 0111 | 7 | 0001 | 1 |
| -6 | 1010 | 0110 | 6 | 0010 | 2 |
| -5 | 1011 | 0101 | 5 | 0001 | 1 |
| -4 | 1100 | 0100 | 4 | 0100 | 4 |
| -3 | 1101 | 0011 | 3 | 0001 | 1 |
| -2 | 1110 | 0010 | 2 | 0010 | 2 |
| -1 | 1111 | 0001 | 1 | 0001 | 1 |
Observaciones:
- Puede conjeturar que
i&-isolo tiene un conjunto de bits (es una potencia de 2) y coincide con el conjunto de bits menos significativo dei. -
i + (i&-i)tiene la propiedad interesante de estar un poco más cerca de la siguiente potencia de dos. -
i += (i&-i)establece el bit de desarmado menos significativo dei.
Entonces, haciendo i += (i&-i); eventualmente te hará saltar a la siguiente potencia de dos:
| i | i&-i | sum | | i | i&-i | sum |
+---+------+-----+ +---+------+-----+
| 1 | 1 | 2 | | 5 | 1 | 6 |
| 2 | 2 | 4 | | 6 | 2 | -8 |
| 4 | 4 | -8 | |-8 | -8 | UB |
|-8 | -8 | UB |
| i | i&-i | sum | | i | i&-i | sum |
+---+------+-----+ +---+------+-----+
| 3 | 1 | 4 | | 7 | 1 | -8 |
| 4 | 4 | -8 | |-8 | -8 | UB |
|-8 | -8 | UB |
UB: el desbordamiento de un entero con signo exhibe un comportamiento indefinido.
La expresión i & -i se basa en el Complemento de Dos que se utiliza para representar enteros negativos. En pocas palabras, devuelve un valor k donde cada bit excepto el bit menos significativo se establece en 0 , pero ese bit menos significativo mantiene su propio valor. (es decir 1 )
Siempre que la expresión que proporcionó se ejecute en un sistema en el que el Complemento de Dos se utiliza para representar enteros negativos, sería portátil. Entonces, la respuesta a su segunda pregunta sería que la expresión depende de la representación de enteros negativos.
Para responder a su primera pregunta, dado que las expresiones aritméticas dependen de los tipos de datos y sus representaciones, no creo que haya una única expresión aritmética que sea equivalente a la expresión i & -i . En esencia, el siguiente código sería equivalente en funcionalidad a esa expresión. (Suponiendo que i sea del tipo int ). Sin embargo, tenga en cuenta que tuve que hacer uso de un bucle para producir la misma funcionalidad, y no solo aritmética.
int tmp = 0, k = 0;
while(tmp < 32)
{
if(i & (1 << tmp))
{
k = i & (1 << tmp);
break;
}
tmp++;
}
i += k;
La forma más fácil de pensarlo es en términos de equivalencia matemática:
-i == (~i + 1)
Entonces -i invierte los bits del valor y luego agrega 1 . El significado de esto es que todos los 0 bits más bajos de i se convierten en 1 s mediante la operación ~i , por lo que agregar 1 al valor hace que todos esos bits 1 bajos se conviertan en 0 mientras se lleva el 1 hacia arriba hasta que caiga en una 0 bit, que simplemente será la misma posición que el bit 1 más bajo en i .
Aquí hay un ejemplo para el número 6 (0110 en binario):
i = 0110
~i == 1001
(~i + 1) == 1010
i & (~i + 1) == 0010
Es posible que deba realizar cada operación manualmente varias veces antes de realizar los patrones en los bits.
Aquí hay dos ejemplos más:
i = 1000
~i == 0111
(~i + 1) == 1000
i & (~i + 1) == 1000
i = 1100
~i == 0011
(~i + 1) == 0100
i & (~i + 1) == 0100
¿Ves cómo el + 1 causa una especie de ''cascada de bits'' que lleva el uno hasta el primer bit 0 abierto?
Entonces, si (i & -i) es un medio para extraer el bit 1 más bajo, entonces se deduce que los casos de uso de i += (i & -i) y i -= (i & -i) son intentos de agregar y Resta el bit 1 más bajo de un valor.
Restar el 1 bit más bajo de un valor de sí mismo sirve como un medio para poner a cero ese bit.
Agregar el 1 bit más bajo de un valor a sí mismo no parece tener ningún propósito especial, simplemente hace lo que dice en la lata.
Debe ser portátil en cualquier sistema utilizando el complemento de dos.
Si i tiene un tipo sin signo, las expresiones son completamente portátiles y están bien definidas.
Si i tiene tipo firmado, no es portátil, ya que & se define en términos de representaciones, pero unario - , += , y -= se definen en términos de valores. Sin embargo, si la próxima versión de los mandatos estándar de C ++ se complementa , se convertirá en portátil y hará lo mismo que en el caso sin firma.
En el caso sin firma (y el caso del complemento de dos), es fácil confirmar que i&-i es una potencia de dos (tiene solo un bit distinto de cero), y tiene el mismo valor que el bit de posición más bajo de i (que también es el bit más bajo de -i ). Por lo tanto:
-
i -= i&-i;borra el bit de conjunto más bajo dei. -
i += i&-i;incrementos (borrado, pero con llevar a bits más altos) el bit de conjunto más bajo dei.
Para los tipos sin firmar nunca hay desbordamiento para ninguna expresión. Para los tipos con signo, i -= i&-i desborda tomando -i cuando inicialmente tengo el valor mínimo del tipo, y i += i&-i desborda en el += cuando i inicialmente tiene el valor máximo del tipo.
i & -i es la forma más fácil de obtener el bit menos significativo (LSB) para un entero i .
Puedes leer más here .
A1: Puede leer más sobre ''Equivalentes matemáticos'' here .
A2: Si la representación de enteros negativos no es la forma estándar usual (es decir, enteros grandes extraños), entonces i & -i podría no ser LSB.