La operación bit a bit resulta en un tamaño variable inesperado

24

Contexto

Estamos transfiriendo el código C que se compiló originalmente usando un compilador C de 8 bits para el microcontrolador PIC. Un modismo común que se utilizó para evitar que las variables globales sin signo (por ejemplo, contadores de error) vuelvan a cero es el siguiente:

if(~counter) counter++;

El operador bit a bit aquí invierte todos los bits y la declaración solo es verdadera si counteres menor que el valor máximo. Es importante destacar que esto funciona independientemente del tamaño variable.

Problema

Ahora estamos apuntando a un procesador ARM de 32 bits usando GCC. Hemos notado que el mismo código produce resultados diferentes. Hasta donde podemos ver, parece que la operación de complemento a nivel de bits devuelve un valor que es de un tamaño diferente al que esperaríamos. Para reproducir esto, compilamos, en GCC:

uint8_t i = 0;
int sz;

sz = sizeof(i);
printf("Size of variable: %d\n", sz); // Size of variable: 1

sz = sizeof(~i);
printf("Size of result: %d\n", sz); // Size of result: 4

En la primera línea de salida, obtenemos lo que esperaríamos: ies 1 byte. Sin embargo, el complemento bit a bit ies en realidad de cuatro bytes, lo que causa un problema porque las comparaciones con esto ahora no darán los resultados esperados. Por ejemplo, si está haciendo (donde ise inicializa correctamente uint8_t):

if(~i) i++;

Veremos i"ajuste" desde 0xFF hasta 0x00. Este comportamiento es diferente en GCC en comparación con cuando solía funcionar como pretendíamos en el compilador anterior y el microcontrolador PIC de 8 bits.

Somos conscientes de que podemos resolver esto lanzando así:

if((uint8_t)~i) i++;

O por

if(i < 0xFF) i++;

Sin embargo, en estas dos soluciones, el tamaño de la variable debe ser conocido y es propenso a errores para el desarrollador de software. Este tipo de comprobaciones de límites superiores se producen en toda la base de código. Hay varios tamaños de variables (por ejemplo., uint16_tY unsigned charetc.) y el cambio de éstos en una base de código de otra forma de trabajo no es algo que estamos deseando.

Pregunta

¿Nuestra comprensión del problema es correcta, y hay opciones disponibles para resolver esto que no requieren volver a visitar cada caso en el que hemos usado este idioma? ¿Es correcta nuestra suposición de que una operación como complemento a nivel de bits debería devolver un resultado que tenga el mismo tamaño que el operando? Parece que esto se rompería, dependiendo de las arquitecturas del procesador. Siento que estoy tomando pastillas locas y que C debería ser un poco más portátil que esto. Nuevamente, nuestra comprensión de esto podría estar equivocada.

En la superficie, esto puede no parecer un gran problema, pero este idioma que funcionaba anteriormente se usa en cientos de ubicaciones y estamos ansiosos por entender esto antes de proceder con cambios costosos.


Nota: Aquí hay una pregunta duplicada aparentemente similar pero no exacta: la operación bit a bit en char da un resultado de 32 bits

No vi el quid de la cuestión discutido allí, a saber, el tamaño del resultado de un complemento bit a bit diferente de lo que se pasa al operador.

Sales de Charlie
fuente
14
"¿Es correcta nuestra suposición de que una operación como complemento a nivel de bits debería devolver un resultado que tenga el mismo tamaño que el operando?" No, esto no es correcto, se aplican promociones enteras.
Thomas Jager
2
Aunque ciertamente es relevante, no estoy convencido de que sean duplicados de esta pregunta en particular, porque no brindan una solución al problema.
Cody Gray
3
Siento que estoy tomando pastillas locas y que C debería ser un poco más portátil que esto. Si no obtuvo promociones de enteros en tipos de 8 bits, entonces su compilador no era compatible con el estándar C. En ese caso, creo que debería revisar todos los cálculos para verificarlos y corregirlos si es necesario.
user694733
1
¿Soy el único que se pregunta qué lógica, aparte de los contadores realmente sin importancia, puede llevarlo a "aumentar si hay espacio suficiente, de lo contrario olvídalo"? Si está transfiriendo código, ¿puede usar int (4 bytes) en lugar de uint_8? Eso evitaría su problema en muchos casos.
disco
1
@puck Tienes razón, podríamos cambiarlo a 4 bytes, pero rompería la compatibilidad al comunicarse con los sistemas existentes. La intención es saber cuándo hay algún error y, por lo tanto, un contador de 1 byte era originalmente suficiente y sigue siéndolo.
Charlie sale el

Respuestas:

26

Lo que está viendo es el resultado de promociones enteras . En la mayoría de los casos en los que se usa un valor entero en una expresión, si el tipo del valor es más pequeño de lo que intse promueve int. Esto se documenta en la sección 6.3.1.1p2 del estándar C :

Lo siguiente se puede usar en una expresión donde intsea unsigned intque se use

  • Un objeto o expresión con un tipo entero (distinto de into unsigned int) cuyo rango de conversión de enteros es menor o igual que el rango de inty unsigned int.
  • Un campo de tipo de bit _Bool, int ,firmado int , orunsigned int`.

Si un intpuede representar todos los valores del tipo original (como está restringido por el ancho, para un campo de bits), el valor se convierte en un int; de lo contrario, se convierte en un unsigned int. Estas se llaman promociones enteras . Todos los demás tipos no se modifican con las promociones de enteros.

Entonces, si una variable tiene tipo uint8_ty el valor 255, el uso de cualquier operador que no sea una conversión o asignación en ella primero la convertirá a tipo intcon el valor 255 antes de realizar la operación. Es por eso que sizeof(~i)le da 4 en lugar de 1.

La Sección 6.5.3.3 describe que las promociones de enteros se aplican al ~operador:

El resultado del ~operador es el complemento a nivel de bit de su operando (promovido) (es decir, cada bit en el resultado se establece si y solo si el bit correspondiente en el operando convertido no se establece). Las promociones de enteros se realizan en el operando y el resultado tiene el tipo promocionado. Si el tipo promocionado es un tipo sin signo, la expresión ~Ees equivalente al valor máximo representable en ese tipo menos E.

Entonces, suponiendo un valor de 32 bits int, si countertiene el valor de 8 bits 0xff, se convierte en el valor de 32 bits 0x000000ff, y aplicarlo ~le da 0xffffff00.

Probablemente, la forma más sencilla de manejar esto es sin tener que saber el tipo es verificar si el valor es 0 después de incrementar, y si es así disminuirlo.

if (!++counter) counter--;

La envoltura de los enteros sin signo funciona en ambas direcciones, por lo que disminuir un valor de 0 le da el mayor valor positivo.

dbush
fuente
1
if (!++counter) --counter;podría ser menos extraño para algunos programadores que usar el operador de coma.
Eric Postpischil
1
Otra alternativa es ++counter; counter -= !counter;.
Eric Postpischil
@EricPostpischil En realidad, me gusta más tu primera opción. Editado
dbush
15
Esto es feo e ilegible, no importa cómo lo escribas. Si tiene que usar un modismo como este, haga un favor a cada programador de mantenimiento y envuélvalo como una función en línea : algo así como increment_unsigned_without_wraparoundo increment_with_saturation. Personalmente, usaría una función genérica de tres operandos clamp.
Cody Gray
55
Además, no puede hacer que esto sea una función, ya que tiene que comportarse de manera diferente para diferentes tipos de argumentos. Tendría que usar una macro de tipo genérico .
user2357112 es compatible con Monica
7

en tamaño de (i); solicita el tamaño de la variable i , entonces 1

en tamaño de (~ i); solicita el tamaño del tipo de expresión, que es un int , en su caso 4


Usar

si (~ i)

saber si yo no valora 255 (en su caso con un el uint8_t) no es muy legible, acaba de hacer

if (i != 255)

y tendrás un código portátil y legible


Hay múltiples tamaños de variables (por ejemplo, uint16_t y unsigned char, etc.)

Para administrar cualquier tamaño de sin firmar:

if (i != (((uintmax_t) 2 << (sizeof(i)*CHAR_BIT-1)) - 1))

La expresión es constante, por lo que se calcula en tiempo de compilación.

#include <limits.h> para CHAR_BIT y #include <stdint.h> para uintmax_t

bruno
fuente
3
La pregunta establece explícitamente que tienen varios tamaños para tratar, por lo que != 255es inadecuado.
Eric Postpischil
@EricPostpischil ah sí, lo olvido, así que "if (i! = ((1u << sizeof (i) * 8) - 1))" suponiendo que siempre esté sin firmar?
bruno
1
Eso no estará definido para los unsignedobjetos, ya que los desplazamientos de todo el ancho del objeto no están definidos por el estándar C, pero se puede solucionar con (2u << sizeof(i)*CHAR_BIT-1) - 1.
Eric Postpischil
oh sí ofc, CHAR_BIT, mi malo
bruno
2
Por seguridad con tipos más anchos, uno podría usar ((uintmax_t) 2 << sizeof(i)*CHAR_BIT-1) - 1.
Eric Postpischil
5

Aquí hay varias opciones para implementar "Agregar 1 a xpero fijar en el valor máximo representable", dado que xes un tipo entero sin signo:

  1. Agregue uno si y solo si xes menor que el valor máximo representable en su tipo:

    x += x < Maximum(x);

    Consulte el siguiente elemento para la definición de Maximum. Este método tiene una buena posibilidad de ser optimizado por un compilador para obtener instrucciones eficientes, como una comparación, alguna forma de conjunto o movimiento condicional, y un complemento.

  2. Compare con el valor más grande del tipo:

    if (x < ((uintmax_t) 2u << sizeof x * CHAR_BIT - 1) - 1) ++x

    (Esto calcula 2 N , donde N es el número de bits x, al cambiar 2 por N −1 bits. Hacemos esto en lugar de cambiar 1 N bits porque un cambio por el número de bits en un tipo no está definido por C estándar. La CHAR_BITmacro puede ser desconocida para algunos; es el número de bits en un byte, también lo sizeof x * CHAR_BITes el número de bits en el tipo de x).

    Esto se puede envolver en una macro según se desee por estética y claridad:

    #define Maximum(x) (((uintmax_t) 2u << sizeof (x) * CHAR_BIT - 1) - 1)
    if (x < Maximum(x)) ++x;
    
  3. Incremente xy corrija si se ajusta a cero, usando un if:

    if (!++x) --x; // !++x is true if ++x wraps to zero.
  4. Incremente xy corrija si se ajusta a cero, usando una expresión:

    ++x; x -= !x;

    Esto es nominalmente sin ramificación (a veces beneficioso para el rendimiento), pero un compilador puede implementarlo de la misma manera anterior, utilizando una ramificación si es necesario pero posiblemente con instrucciones incondicionales si la arquitectura de destino tiene instrucciones adecuadas.

  5. Una opción sin ramificación, utilizando la macro anterior, es:

    x += 1 - x/Maximum(x);

    Si xes el máximo de su tipo, esto se evalúa como x += 1-1. De lo contrario, lo es x += 1-0. Sin embargo, la división es algo lenta en muchas arquitecturas. Un compilador puede optimizar esto a instrucciones sin división, dependiendo del compilador y la arquitectura de destino.

Eric Postpischil
fuente
1
Simplemente no me atrevo a votar una respuesta que recomienda usar una macro. C tiene funciones en línea. No está haciendo nada dentro de esa definición de macro que no se puede hacer fácilmente dentro de una función en línea. Y si va a utilizar una macro, asegúrese de paréntesis estratégicamente para mayor claridad: el operador << tiene muy poca prioridad. Clang advierte sobre esto con -Wshift-op-parentheses. La buena noticia es que un compilador optimizador no va a generar una división aquí, por lo que no tiene que preocuparse de que sea lento.
Cody Gray
1
@CodyGray, si crees que puedes hacer esto con una función, escribe una respuesta.
Carsten S
2
@CodyGray: sizeof xno se puede implementar dentro de una función C porque xdebería ser un parámetro (u otra expresión) con algún tipo fijo. No pudo producir el tamaño del tipo de argumento que usa la persona que llama. Una lata de macro.
Eric Postpischil
2

Antes de stdint.h, los tamaños de las variables pueden variar de un compilador a otro y los tipos de variables reales en C siguen siendo int, long, etc. y el autor del compilador los define como su tamaño. No algunos supuestos específicos estándar o específicos. El (los) autor (es) deben crear stdint.h para mapear los dos mundos, ese es el propósito de stdint.h para mapear uint_this that a int, long, short.

Si está transfiriendo código de otro compilador y usa char, short, int, long, debe pasar por cada tipo y realizar el puerto usted mismo, no hay forma de evitarlo. Y si termina con el tamaño correcto para la variable, la declaración cambia pero el código como está escrito funciona ...

if(~counter) counter++;

o ... suministre la máscara o el tipografía directamente

if((~counter)&0xFF) counter++;
if((uint_8)(~counter)) counter++;

Al final del día, si desea que este código funcione, debe portarlo a la nueva plataforma. Tu elección de cómo. Sí, debe dedicar el tiempo necesario a cada caso y hacerlo bien; de lo contrario, volverá a este código, que es aún más costoso.

Si aísla los tipos de variables en el código antes de portarlos y el tamaño de los tipos de variables, entonces aísle las variables que hacen esto (debería ser fácil de asimilar) y cambie sus declaraciones usando definiciones stdint.h que, con suerte, no cambiarán en el futuro, y te sorprenderías, pero a veces se usan los encabezados incorrectos, así que incluso pon cheques para que puedas dormir mejor por la noche

if(sizeof(uint_8)!=1) return(FAIL);

Y si bien ese estilo de codificación funciona (if (~ counter) counter ++;), para la portabilidad desea ahora y en el futuro, es mejor usar una máscara para limitar específicamente el tamaño (y no confiar en la declaración), haga esto cuando el código se escribe en primer lugar o simplemente termina el puerto y luego no tendrás que volver a portarlo algún otro día. O para que el código sea más legible, haga x <0xFF entonces o x! = 0xFF o algo así, entonces el compilador puede optimizarlo en el mismo código que cualquiera de estas soluciones, solo lo hace más legible y menos riesgoso ...

Depende de lo importante que sea el producto o cuántas veces desee enviar parches / actualizaciones o hacer rodar un camión o caminar al laboratorio para solucionar el problema, ya sea que intente encontrar una solución rápida o simplemente toque las líneas de código afectadas. si solo son cien o pocos, no es un puerto tan grande.

viejo contador de tiempo
fuente
0
6.5.3.3 Operadores aritméticos unarios
...
4 El resultado del ~operador es el complemento bit a bit de su operando (promovido) (es decir, cada bit en el resultado se establece si y solo si el bit correspondiente en el operando convertido no se establece ) Las promociones enteras se realizan en el operando y el resultado tiene el tipo promocionado . Si el tipo promocionado es un tipo sin signo, la expresión ~Ees equivalente al valor máximo representable en ese tipo menos E.

C 2011 Borrador en línea

El problema es que el operando de ~se está promoviendo intantes de que se aplique el operador.

Desafortunadamente, no creo que haya una manera fácil de salir de esto. Escritura

if ( counter + 1 ) counter++;

no ayudará porque las promociones se aplican allí también. Lo único que puedo sugerir es crear algunas constantes simbólicas para el valor máximo que desea que represente ese objeto y probar eso:

#define MAX_COUNTER 255
...
if ( counter < MAX_COUNTER-1 ) counter++;
John Bode
fuente
Aprecio el punto sobre la promoción de enteros: parece que este es el problema con el que nos encontramos. Sin embargo, vale la pena señalar que, en su segundo ejemplo de código, -1no es necesario, ya que esto provocaría que el contador se establezca en 254 (0xFE). En cualquier caso, este enfoque, como se mencionó en mi pregunta, no es ideal debido a los diferentes tamaños variables en la base de código que participan en este idioma.
Charlie sale el