¿Está permitida esta optimización de punto flotante?

90

Intenté comprobar dónde floatpierde la capacidad de representar exactamente números enteros grandes. Así que escribí este pequeño fragmento:

int main() {
    for (int i=0; ; i++) {
        if ((float)i!=i) {
            return i;
        }
    }
}

Este código parece funcionar con todos los compiladores, excepto clang. Clang genera un bucle infinito simple. Godbolt .

Esta permitido? Si es así, ¿es un problema de QoI?

geza
fuente
@geza ¡Me interesaría escuchar el número resultante!
nada
5
gccrealiza la misma optimización de bucles infinitos si compila con en su -Ofastlugar, por lo que es una optimización que se gccconsidera insegura, pero puede hacerlo.
12345ieee
3
g ++ también genera un bucle infinito, pero no optimiza el trabajo desde dentro. Puede ver que lo hace ucomiss xmm0,xmm0para compararse (float)iconsigo mismo. Esa fue tu primera pista de que tu fuente de C ++ no significa lo que pensabas. ¿Está afirmando que tiene este bucle para imprimir / devolver 16777216? ¿Con qué compilador / versión / opciones fue eso? Porque eso sería un error del compilador. gcc optimiza correctamente su código jnpcomo la rama del bucle ( godbolt.org/z/XJYWeu ): siga repitiendo mientras los operandos != no sean NaN.
Peter Cordes
4
Específicamente, es la -ffast-mathopción que está habilitada implícitamente -Ofastque permite a GCC aplicar optimizaciones de punto flotante inseguras y así generar el mismo código que Clang. MSVC se comporta exactamente de la misma manera: sin /fp:fast, genera un montón de código que da como resultado un bucle infinito; con /fp:fast, emite una sola jmpinstrucción. Supongo que sin activar explícitamente las optimizaciones de FP inseguras, estos compiladores se cuelgan de los requisitos de IEEE 754 con respecto a los valores de NaN. Es bastante interesante que Clang no lo haga, en realidad. Su analizador estático es mejor. @ 12345ieee
Cody Gray
1
@geza: Si el código hizo lo que pretendías, verificando cuándo el valor matemático de (float) idifiere del valor matemático de i, entonces el resultado (el valor devuelto en la returndeclaración) sería 16.777.217, no 16.777.216.
Eric Postpischil

Respuestas:

49

Como señaló @Angew , el !=operador necesita el mismo tipo en ambos lados. (float)i != ida como resultado la promoción del RHS para que flote también, así que lo hemos hecho (float)i != (float)i.


g ++ también genera un bucle infinito, pero no optimiza el trabajo desde dentro. Puede ver que convierte int-> float con cvtsi2ssy se ucomiss xmm0,xmm0compara (float)iconsigo mismo. (Esa fue su primera pista de que su fuente de C ++ no significa lo que pensaba que hacía, como explica la respuesta de @ Angew).

x != xsolo es cierto cuando está "desordenado" porque xera NaN. (se INFINITYcompara a sí mismo en matemáticas IEEE, pero NaN no. NAN == NANes falso, NAN != NANes verdadero).

gcc7.4 y versiones anteriores optimizan correctamente su código jnpcomo la rama del bucle ( https://godbolt.org/z/fyOhW1 ): siga repitiendo mientras los operandos x != x no sean NaN. (gcc8 y versiones posteriores también verifican jeuna ruptura del bucle, sin optimizar en función del hecho de que siempre será cierto para cualquier entrada que no sea NaN). x86 FP compara set PF en desordenado.


Y por cierto, eso significa que la optimización de clang también es segura : solo tiene que CSE (float)i != (implicit conversion to float)icomo lo mismo, y demostrar que i -> floatnunca es NaN para el rango posible de int.

(Aunque dado que este bucle llegará a UB de desbordamiento firmado, se le permite emitir literalmente cualquier conjunto que desee, incluida una ud2instrucción ilegal, o un bucle infinito vacío independientemente de cuál sea el cuerpo del bucle en realidad). Pero ignorando el UB de desbordamiento firmado , esta optimización sigue siendo 100% legal.


GCC no puede optimizar el cuerpo del bucle incluso -fwrapvpara hacer que el desbordamiento de enteros con signo esté bien definido (como envoltura de complemento a 2). https://godbolt.org/z/t9A8t_

Incluso habilitar -fno-trapping-mathno ayuda. ( Desafortunadamente, el valor predeterminado de GCC es habilitar
-ftrapping-matha pesar de que la implementación de GCC está rota / con errores ). Int-> conversión flotante puede causar una excepción FP inexacta (para números demasiado grandes para ser representados exactamente), por lo que con excepciones posiblemente desenmascaradas, es razonable no hacerlo optimizar el cuerpo del lazo. (Porque la conversión 16777217a flotante podría tener un efecto secundario observable si se desenmascara la excepción inexacta).

Pero con -O3 -fwrapv -fno-trapping-math, es una optimización 100% perdida no compilar esto en un bucle infinito vacío. Sin #pragma STDC FENV_ACCESS ON, el estado de las banderas adhesivas que registran las excepciones de FP enmascaradas no es un efecto secundario observable del código. No int-> la floatconversión puede resultar en NaN, por x != xlo que no puede ser cierto.


Todos estos compiladores están optimizando para implementaciones de C ++ que usan IEEE 754 de precisión simple (binary32) floaty 32 bits int.

El bucle corregido(int)(float)i != i tendría UB en implementaciones de C ++ con 16 bits estrechos inty / o más anchos float, porque alcanzaría UB de desbordamiento de enteros con signo antes de llegar al primer entero que no era exactamente representable como float.

Pero UB bajo un conjunto diferente de opciones definidas por la implementación no tiene consecuencias negativas cuando se compila para una implementación como gcc o clang con el x86-64 System V ABI.


Por cierto, podría calcular estáticamente el resultado de este bucle desde FLT_RADIXy FLT_MANT_DIG, definido en <climits>. O al menos puede floathacerlo en teoría, si realmente se ajusta al modelo de un flotante IEEE en lugar de algún otro tipo de representación de números reales como Posit / unum.

No estoy seguro de cuánto define el estándar ISO C ++ sobre el floatcomportamiento y si un formato que no se basó en un exponente de ancho fijo y campos significativos sería compatible con los estándares.


En comentarios:

@geza ¡Me interesaría escuchar el número resultante!

@nada: es 16777216

¿Está afirmando que tiene este bucle para imprimir / devolver 16777216?

Actualización: dado que ese comentario ha sido eliminado, creo que no. Probablemente el OP solo esté citando floatantes del primer número entero que no se puede representar exactamente como un 32 bits float. https://en.wikipedia.org/wiki/Single-precision_floating-point_format#Precision_limits_on_integer_values, es decir, lo que esperaban verificar con este código defectuoso.

Por supuesto, la versión corregida se imprimirá 16777217, el primer número entero que no es exactamente representable, en lugar del valor anterior.

(Todos los valores flotantes más altos son números enteros exactos, pero son múltiplos de 2, luego 4, luego 8, etc. para valores de exponentes más altos que el ancho significativo. Se pueden representar muchos valores enteros más altos, pero 1 unidad en el último lugar (del significado) es mayor que 1, por lo que no son números enteros contiguos. El finito más grande floatestá justo por debajo de 2 ^ 128, que es demasiado grande para par int64_t).

Si algún compilador saliera del bucle original e imprimiera eso, sería un error del compilador.

Peter Cordes
fuente
3
@SombreroChicken: no, primero aprendí electrónica (de algunos libros de texto que mi papá tenía por ahí; era profesor de física), luego lógica digital y después de eso me metí en CPU / software. : P Siempre me ha gustado entender las cosas desde cero, o si empiezo con un nivel superior, me gusta aprender al menos algo sobre el nivel inferior que influya en cómo / por qué funcionan las cosas en el nivel al que estoy. pensando en. (por ejemplo, cómo funciona ASM y cómo optimizarlo está influenciado por las restricciones de diseño de la CPU / cosas de la arquitectura de la CPU. Lo que a su vez proviene de la física + matemáticas).
Peter Cordes
1
Es posible que GCC no pueda optimizar incluso con frapw, pero estoy seguro de que GCC 10 -ffinite-loopsfue diseñado para situaciones como esta.
MCCCS
64

Tenga en cuenta que el operador integrado !=requiere que sus operandos sean del mismo tipo y lo logrará utilizando promociones y conversiones si es necesario. En otras palabras, su condición es equivalente a:

(float)i != (float)i

Eso nunca debería fallar, por lo que el código eventualmente se desbordará i, dando a su programa un comportamiento indefinido. Por tanto, cualquier comportamiento es posible.

Para verificar correctamente lo que desea verificar, debe devolver el resultado a int:

if ((int)(float)i != i)
Angew ya no está orgulloso de SO
fuente
8
@ Džuris Es UB. No es sin un resultado definitivo. El compilador podría darse cuenta de que solo puede terminar en UB y decidir eliminar el bucle por completo.
Financia la demanda de Monica el
4
@opa ¿quieres decir static_cast<int>(static_cast<float>(i))? reinterpret_castes obvio UB allí
Caleth
6
@NicHartley: ¿Estás diciendo que (int)(float)i != ies UB? ¿Cómo concluyes eso? Sí, depende de las propiedades definidas por la implementación (porque floatno se requiere que sea IEEE754 binary32), pero en cualquier implementación dada está bien definido a menos que floatpueda representar exactamente todos los intvalores positivos , por lo que obtenemos UB de desbordamiento de enteros con signo. ( en.cppreference.com/w/cpp/types/climits define FLT_RADIXy FLT_MANT_DIGdetermina esto). En general, imprimir cosas definidas por la implementación, como std::cout << sizeof(int)no es UB ...
Peter Cordes
2
@Caleth: reinterpret_cast<int>(float)no es exactamente UB, es solo un error de sintaxis / mal formado. Sería bueno si esa sintaxis permitiera el type-punning de float intcomo alternativa a memcpy(que está bien definido), pero reinterpret_cast<>creo que solo funciona en tipos de puntero.
Peter Cordes
2
@Peter Solo por NaN, x != xes cierto. Ver en vivo por coliru . En C también.
Desduplicador