Intenté comprobar dónde float
pierde 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?
c++
floating-point
clang
geza
fuente
fuente
gcc
realiza la misma optimización de bucles infinitos si compila con en su-Ofast
lugar, por lo que es una optimización que segcc
considera insegura, pero puede hacerlo.ucomiss xmm0,xmm0
para compararse(float)i
consigo 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 / devolver16777216
? ¿Con qué compilador / versión / opciones fue eso? Porque eso sería un error del compilador. gcc optimiza correctamente su códigojnp
como la rama del bucle ( godbolt.org/z/XJYWeu ): siga repitiendo mientras los operandos!=
no sean NaN.-ffast-math
opción que está habilitada implícitamente-Ofast
que 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 solajmp
instrucció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(float) i
difiere del valor matemático dei
, entonces el resultado (el valor devuelto en lareturn
declaración) sería 16.777.217, no 16.777.216.Respuestas:
Como señaló @Angew , el
!=
operador necesita el mismo tipo en ambos lados.(float)i != i
da 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
cvtsi2ss
y seucomiss xmm0,xmm0
compara(float)i
consigo 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 != x
solo es cierto cuando está "desordenado" porquex
era NaN. (seINFINITY
compara a sí mismo en matemáticas IEEE, pero NaN no.NAN == NAN
es falso,NAN != NAN
es verdadero).gcc7.4 y versiones anteriores optimizan correctamente su código
jnp
como la rama del bucle ( https://godbolt.org/z/fyOhW1 ): siga repitiendo mientras los operandosx != x
no sean NaN. (gcc8 y versiones posteriores también verificanje
una 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)i
como lo mismo, y demostrar quei -> float
nunca es NaN para el rango posible deint
.(Aunque dado que este bucle llegará a UB de desbordamiento firmado, se le permite emitir literalmente cualquier conjunto que desee, incluida una
ud2
instrucció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
-fwrapv
para 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-math
no ayuda. ( Desafortunadamente, el valor predeterminado de GCC es habilitar-ftrapping-math
a 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ón16777217
a 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. Noint
-> lafloat
conversión puede resultar en NaN, porx != x
lo que no puede ser cierto.Todos estos compiladores están optimizando para implementaciones de C ++ que usan IEEE 754 de precisión simple (binary32)
float
y 32 bitsint
.El bucle corregido
(int)(float)i != i
tendría UB en implementaciones de C ++ con 16 bits estrechosint
y / o más anchosfloat
, porque alcanzaría UB de desbordamiento de enteros con signo antes de llegar al primer entero que no era exactamente representable comofloat
.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_RADIX
yFLT_MANT_DIG
, definido en<climits>
. O al menos puedefloat
hacerlo 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
float
comportamiento 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:
¿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
float
antes del primer número entero que no se puede representar exactamente como un 32 bitsfloat
. 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
float
está justo por debajo de 2 ^ 128, que es demasiado grande para parint64_t
).Si algún compilador saliera del bucle original e imprimiera eso, sería un error del compilador.
fuente
frapw
, pero estoy seguro de que GCC 10-ffinite-loops
fue diseñado para situaciones como esta.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)
fuente
static_cast<int>(static_cast<float>(i))
?reinterpret_cast
es obvio UB allí(int)(float)i != i
es UB? ¿Cómo concluyes eso? Sí, depende de las propiedades definidas por la implementación (porquefloat
no se requiere que sea IEEE754 binary32), pero en cualquier implementación dada está bien definido a menos quefloat
pueda representar exactamente todos losint
valores positivos , por lo que obtenemos UB de desbordamiento de enteros con signo. ( en.cppreference.com/w/cpp/types/climits defineFLT_RADIX
yFLT_MANT_DIG
determina esto). En general, imprimir cosas definidas por la implementación, comostd::cout << sizeof(int)
no es UB ...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 floatint
como alternativa amemcpy
(que está bien definido), peroreinterpret_cast<>
creo que solo funciona en tipos de puntero.x != x
es cierto. Ver en vivo por coliru . En C también.