(A + B + C) ≠ (A + C + B) y reordenamiento del compilador


108

Agregar dos enteros de 32 bits puede resultar en un desbordamiento de enteros:

uint64_t u64_z = u32_x + u32_y;

Este desbordamiento se puede evitar si uno de los enteros de 32 bits se convierte primero o se agrega a un entero de 64 bits.

uint64_t u64_z = u32_x + u64_a + u32_y;

Sin embargo, si el compilador decide reordenar la adición:

uint64_t u64_z = u32_x + u32_y + u64_a;

el desbordamiento de enteros aún podría ocurrir.

¿Se permite a los compiladores hacer tal reordenación o podemos confiar en que noten la inconsistencia del resultado y mantengan el orden de las expresiones como está?


15
En realidad, no muestra un desbordamiento de enteros porque parece que se agregan uint32_tvalores, que no se desbordan, se ajustan. Estos no son comportamientos diferentes.
Martin Bonner apoya a Monica el

5
Consulte la sección 1.9 de los estándares c ++, responde directamente a su pregunta (incluso hay un ejemplo que es casi exactamente igual al suyo).
Holt

3
@Tal: Como ya han dicho otros: no hay desbordamiento de enteros. Unsigned se define para envolver, para firmado es un comportamiento indefinido, por lo que cualquier implementación servirá, incluidos los demonios nasales.
demasiado honesto para este sitio

5
@Tal: ¡Tonterías! Como ya escribí: el estándar es muy claro y requiere envolver, no saturar (eso sería posible con firmado, ya que es UB como estándar.
demasiado honesto para este sitio

15
@rustyx: Ya sea que lo llame envoltura o desbordamiento, el punto permanece que ((uint32_t)-1 + (uint32_t)1) + (uint64_t)0da como resultado 0, mientras que (uint32_t)-1 + ((uint32_t)1 + (uint64_t)0)da como resultado 0x100000000, y estos dos valores no son iguales. Por tanto, es importante que el compilador pueda aplicar esa transformación o no. Pero sí, el estándar solo usa la palabra "desbordamiento" para enteros con signo, no sin signo.
Steve Jessop

Respuestas:


84

Si el optimizador realiza tal reordenación, todavía está vinculado a la especificación C, por lo que dicha reordenación se convertiría en:

uint64_t u64_z = (uint64_t)u32_x + (uint64_t)u32_y + u64_a;

Razón fundamental:

Empezamos con

uint64_t u64_z = u32_x + u64_a + u32_y;

La suma se realiza de izquierda a derecha.

Las reglas de promoción de enteros establecen que en la primera adición de la expresión original, u32_xse promoverá a uint64_t. En la segunda adición, u32_ytambién será ascendido a uint64_t.

Por lo tanto, para cumplir con la especificación C, cualquier optimizador debe promover u32_xy u32_yvalores sin signo de 64 bits. Esto es equivalente a agregar un elenco. (La optimización real no se realiza en el nivel C, pero yo uso la notación C porque es una notación que entendemos).


¿No es asociativo de izquierda, entonces (u32_x + u32_t) + u64_a?
Inútil

12
@Useless: Klas emitió todo a 64 bits. Ahora el orden no hace ninguna diferencia. El compilador no necesita seguir la asociatividad, solo tiene que producir exactamente el mismo resultado que si lo hiciera.
gnasher729

2
Parece sugerir que el código de OP se evaluaría así, lo cual no es cierto.
Inútil

@Klas: ¿le importaría explicar por qué este es el caso y cómo llega exactamente a su muestra de código?
rustyx

1
@rustyx Necesitaba una explicación. Gracias por presionarme para agregar uno.
Klas Lindbäck

28

Un compilador solo puede reordenar bajo la regla como si . Es decir, si el reordenamiento siempre dará el mismo resultado que el pedido especificado, entonces está permitido. De lo contrario (como en su ejemplo), no.

Por ejemplo, dada la siguiente expresión

i32big1 - i32big2 + i32small

que ha sido cuidadosamente construido para restar los dos valores que se sabe que son grandes pero similares, y solo luego agregar el otro valor pequeño (evitando así cualquier desbordamiento), el compilador puede optar por reordenar en:

(i32small - i32big2) + i32big1

y confíe en el hecho de que la plataforma de destino está utilizando aritmética de dos complementos con ajuste para evitar problemas. (Este reordenamiento puede ser sensato si se presiona el compilador para los registros y resulta que ya lo tiene i32smallen un registro).


El ejemplo de OP usa tipos sin firmar. i32big1 - i32big2 + i32smallimplica tipos firmados. Entran en juego preocupaciones adicionales.
chux - Reincorporación a Monica

@chux Por supuesto. El punto que estaba tratando de hacer es que, aunque no pude escribir (i32small-i32big2) + i32big1, (porque podría causar UB), el compilador puede reorganizarlo de manera efectiva porque el compilador puede estar seguro de que el comportamiento será correcto.
Martin Bonner apoya a Monica

3
@chux: preocupaciones adicionales como UB no entran en juego, porque estamos hablando de un reordenamiento del compilador bajo la regla como si. Un compilador en particular puede aprovechar el conocimiento de su propio comportamiento de desbordamiento.
MSalters

16

Existe la regla "como si" en C, C ++ y Objective-C: el compilador puede hacer lo que quiera siempre que ningún programa conforme pueda notar la diferencia.

En estos lenguajes, a + b + c se define como lo mismo que (a + b) + c. Si puede notar la diferencia entre esto y, por ejemplo, a + (b + c), entonces el compilador no puede cambiar el orden. Si no puede notar la diferencia, entonces el compilador es libre de cambiar el orden, pero está bien, porque no puede notar la diferencia.

En su ejemplo, con b = 64 bits, ayc de 32 bits, el compilador podría evaluar (b + a) + c o incluso (b + c) + a, porque no podría notar la diferencia, pero no (a + c) + b porque puedes notar la diferencia.

En otras palabras, el compilador no puede hacer nada que haga que su código se comporte de manera diferente de lo que debería. No es necesario que produzca el código que cree que produciría, o que cree que debería producir, pero el código le dará exactamente los resultados que debería.


Pero con una gran advertencia; el compilador es libre de asumir ningún comportamiento indefinido (en este caso desbordamiento). Esto es similar a cómo if (a + 1 < a)se puede optimizar una verificación de desbordamiento .
csiz

7
@csiz ... en variables firmadas . Las variables sin signo tienen una semántica de desbordamiento (wrap-around) bien definida.
Gavin S. Yancey

7

Citando de los estándares :

[Nota: Los operadores se pueden reagrupar de acuerdo con las reglas matemáticas habituales solo cuando los operadores son realmente asociativos o conmutativos.7 Por ejemplo, en el siguiente fragmento int a, b;

/∗ ... ∗/
a = a + 32760 + b + 5;

la declaración de expresión se comporta exactamente igual que

a = (((a + 32760) + b) + 5);

debido a la asociatividad y precedencia de estos operadores. Por lo tanto, el resultado de la suma (a + 32760) se suma a b, y ese resultado se suma a 5, lo que da como resultado el valor asignado a a. En una máquina en la que los desbordamientos producen una excepción y en la que el rango de valores representable por un int es [-32768, + 32767], la implementación no puede reescribir esta expresión como

a = ((a + b) + 32765);

ya que si los valores de ayb fueran, respectivamente, -32754 y -15, la suma a + b produciría una excepción mientras que la expresión original no lo haría; ni se puede reescribir la expresión como

a = ((a + 32765) + b);

o

a = (a + (b + 32765));

dado que los valores para ayb podrían haber sido, respectivamente, 4 y -8 o -17 y 12. Sin embargo, en una máquina en la que los desbordamientos no producen una excepción y en la que los resultados de los desbordamientos son reversibles, la declaración de expresión anterior puede ser reescrito por la implementación de cualquiera de las formas anteriores porque se producirá el mismo resultado. - nota final]


4

¿Se permite a los compiladores hacer tal reordenación o podemos confiar en que noten la inconsistencia del resultado y mantengan el orden de las expresiones como está?

El compilador puede reordenar solo si da el mismo resultado; aquí, como observó, no es así.


Es posible escribir una plantilla de función, si desea una, que promueva todos los argumentos std::common_typeantes de agregar; esto sería seguro y no dependería del orden de los argumentos ni de la conversión manual, pero es bastante complicado.


Sé que se debe usar la conversión explícita, pero deseo conocer el comportamiento de los compiladores cuando dicha conversión se omitió por error.
Tal

1
Como dije, sin casting explícito: la adición de la izquierda se realiza primero, sin promoción integral, y por lo tanto sujeta a envoltura. El resultado de esa adición, posiblemente envuelto, luego se promueve uint64_tpara agregar al valor más a la derecha.
Inútil

Tu explicación sobre la regla como si es totalmente incorrecta. El lenguaje C, por ejemplo, especifica qué operaciones deben ocurrir en una máquina abstracta. La regla "como si" le permite hacer absolutamente lo que quiera siempre que nadie pueda notar la diferencia.
gnasher729

Lo que significa que el compilador puede hacer lo que quiera siempre que el resultado sea el mismo que el determinado por las reglas de conversión aritmética y asociatividad izquierda que se muestran.
Inútil

1

Depende del ancho de bits de unsigned/int.

Los 2 siguientes no son iguales (cuando son unsigned <= 32bits). u32_x + u32_yse convierte en 0.

u64_a = 0; u32_x = 1; u32_y = 0xFFFFFFFF;
uint64_t u64_z = u32_x + u64_a + u32_y;
uint64_t u64_z = u32_x + u32_y + u64_a;  // u32_x + u32_y carry does not add to sum.

Son iguales (cuando son unsigned >= 34bits). Las promociones de enteros provocaron que la u32_x + u32_yadición ocurriera en matemáticas de 64 bits. El orden es irrelevante.

Es UB (cuando unsigned == 33bits). Las promociones de enteros provocaron que la adición ocurriera en matemáticas de 33 bits con signo y el desbordamiento con signo es UB.

¿Se permite a los compiladores hacer tal reordenamiento ...?

(Matemáticas de 32 bits): Reordenar sí, pero deben producirse los mismos resultados, por lo que no se propone reordenar OP. A continuación se muestran los mismos

// Same
u32_x + u64_a + u32_y;
u64_a + u32_x + u32_y;
u32_x + (uint64_t) u32_y + u64_a;
...

// Same as each other below, but not the same as the 3 above.
uint64_t u64_z = u32_x + u32_y + u64_a;
uint64_t u64_z = u64_a + (u32_x + u32_y);

... ¿podemos confiar en que notarán la inconsistencia del resultado y mantendrán el orden de las expresiones como está?

Confíe en sí, pero el objetivo de codificación de OP no es muy claro. ¿Debería u32_x + u32_yllevar contribuir? Si OP quiere esa contribución, el código debe ser

uint64_t u64_z = u64_a + u32_x + u32_y;
uint64_t u64_z = u32_x + u64_a + u32_y;
uint64_t u64_z = u32_x + (u32_y + u64_a);

Pero no

uint64_t u64_z = u32_x + u32_y + u64_a;
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.