c++ - ¿Cómo obligar a GCC a asumir que una expresión de punto flotante no es negativa?
assembly floating-point (4)
Después de aproximadamente una semana, pregunté sobre el asunto en GCC Bugzilla y me proporcionaron una solución que es lo más parecido a lo que tenía en mente.
float test (float x)
{
float y = x*x;
if (std::isless(y, 0.f))
__builtin_unreachable();
return std::sqrt(y);
}
que compiles al siguiente ensamblado:
test(float):
mulss xmm0, xmm0
sqrtss xmm0, xmm0
ret
Sin embargo, todavía no estoy muy seguro de qué sucede exactamente aquí.
Hay casos en los que sabe que una determinada expresión de punto flotante siempre será no negativa.
Por ejemplo, al calcular la longitud de un vector, uno hace
sqrt(a[0]*a[0] + ... + a[N-1]*a[N-1])
(NB:
soy
consciente de
std::hypot
, esto no es relevante para la pregunta), y la expresión debajo de la raíz cuadrada es claramente no negativa.
Sin embargo, GCC
outputs
el siguiente ensamblado para
sqrt(x*x)
:
mulss xmm0, xmm0
pxor xmm1, xmm1
ucomiss xmm1, xmm0
ja .L10
sqrtss xmm0, xmm0
ret
.L10:
jmp sqrtf
Es decir, compara el resultado de
x*x
con cero, y si el resultado no es negativo, realiza la instrucción
sqrtss
; de lo contrario, llama a
sqrtf
.
Entonces, mi pregunta es:
¿cómo puedo forzar a GCC a suponer que
x*x
siempre es no negativo para que
sqrtf
la comparación y la llamada
sqrtf
, sin escribir el ensamblaje en línea?
Deseo enfatizar que estoy interesado en una solución local, y no hacer cosas como
-ffast-math
,
-fno-math-errno
o
-ffinite-math-only
(aunque esto realmente resuelve el problema, gracias a ks1322, Harold y Eric Postpischil en los comentarios).
Además, "forzar a GCC a asumir que
x*x
no es negativo" debe interpretarse como
assert(x*x >= 0.f)
, por lo que esto también excluye el caso de que
x*x
sea NaN.
Estoy de acuerdo con las soluciones específicas del compilador, específicas de la plataforma, específicas de la CPU, etc.
Pase la opción
-fno-math-errno
a gcc.
Esto soluciona el problema sin hacer que su código no sea portátil o dejar el ámbito de ISO / IEC 9899: 2011 (C11).
Lo que hace esta opción no es intentar establecer
errno
cuando falla una función de biblioteca matemática:
-fno-math-errno Do not set "errno" after calling math functions that are executed with a single instruction, e.g., "sqrt". A program that relies on IEEE exceptions for math error handling may want to use this flag for speed while maintaining IEEE arithmetic compatibility. This option is not turned on by any -O option since it can result in incorrect output for programs that depend on an exact implementation of IEEE or ISO rules/specifications for math functions. It may, however, yield faster code for programs that do not require the guarantees of these specifications. The default is -fmath-errno. On Darwin systems, the math library never sets "errno". There is therefore no reason for the compiler to consider the possibility that it might, and -fno-math-errno is the default.
Dado que no parece estar particularmente interesado en las rutinas matemáticas que configuran
errno
, esta parece ser una buena solución.
Puede escribir
assert(x*x >= 0.f)
como una promesa de tiempo de compilación en lugar de una verificación de tiempo de ejecución de la siguiente manera en GNU C:
#include <cmath>
float test1 (float x)
{
float tmp = x*x;
if (!(tmp >= 0.0f))
__builtin_unreachable();
return std::sqrt(tmp);
}
(relacionado:
¿Qué optimizaciones facilita __builtin_unreachable?
También podría envolver
if(!x)__builtin_unreachable()
en una macro y llamarlo
promise()
o algo así).
Pero gcc no sabe cómo aprovechar esa promesa de que
tmp
no es NaN y no es negativo.
Todavía obtenemos (
Godbolt
) la misma secuencia de asm enlatada que comprueba
x>=0
y, de lo contrario, llama a
sqrtf
para establecer
errno
.
Presumiblemente, esa expansión en una comparación y ramificación ocurre después de que pasa otra optimización,
por lo que no ayuda al compilador saber más.
Esta es una optimización perdida en la lógica que alinea especulativamente
sqrt
cuando
-fmath-errno
está habilitado (desafortunadamente activado de forma predeterminada).
En cambio, lo que quieres es
-fno-math-errno
, que es seguro a nivel mundial
Esto es 100% seguro si no confía en las funciones matemáticas que nunca configuran
errno
.
Nadie quiere eso, para eso está la propagación de NaN y / o las banderas adhesivas que registran excepciones de FP enmascaradas.
por ejemplo, acceso
fenv
C99 / C ++ 11 a través de
#pragma STDC FENV_ACCESS ON
y luego funciona como
fetestexcept()
.
Vea el ejemplo en
feclearexcept
que muestra su uso para detectar la división por cero.
El entorno FP es parte del contexto del hilo mientras que
errno
es global.
El soporte para este mal funcionamiento obsoleto no es gratuito;
solo debes desactivarlo a menos que tengas un código antiguo que fue escrito para usarlo.
No lo use en código nuevo: use
fenv
.
Idealmente, el soporte para
-fmath-errno
sería lo más barato posible, pero la rareza de cualquiera que realmente use
__builtin_unreachable()
u otras cosas para descartar una entrada de NaN presumiblemente hizo que no valiera la pena el tiempo del desarrollador para implementar la optimización.
Aún así, podría informar un error de optimización perdido si lo desea.
De hecho, el hardware de FPU del mundo real tiene estos indicadores fijos que permanecen configurados hasta que se borran, por ejemplo,
el
mxcsr
estado / control
mxcsr
de x86
para matemáticas SSE / AVX o FPU de hardware en otras ISA.
En el hardware donde la FPU puede detectar excepciones, una implementación de C ++ de calidad admitirá cosas como
fetestexcept()
.
Y si no, entonces las matemáticas probablemente tampoco funcionen.
errno
for math era un diseño antiguo y obsoleto con el que C / C ++ todavía está atascado de forma predeterminada, y ahora se considera una mala idea.
A los compiladores les resulta más difícil integrar las funciones matemáticas de manera eficiente.
O tal vez no estamos tan atascados con eso como pensé:
¿por qué errno no está configurado en EDOM, incluso sqrt elimina el argumento de dominio?
explica que configurar errno en funciones matemáticas es
opcional
en ISO C11, y una implementación puede indicar si lo hacen o no.
Presumiblemente en C ++ también.
Es un gran error agrupar
-fno-math-errno
con optimizaciones de cambio de valor como
-ffast-math
o
-ffinite-math-only
.
Debería considerar habilitarlo globalmente, o al menos para todo el archivo que contiene esta función.
float test2 (float x)
{
return std::sqrt(x*x);
}
# g++ -fno-math-errno -std=gnu++17 -O3
test2(float): # and test1 is the same
mulss xmm0, xmm0
sqrtss xmm0, xmm0
ret
También podría usar
-fno-trapping-math
, si nunca va a desenmascarar ninguna excepción de FP con
feenableexcept()
.
(Aunque esa opción no es necesaria para esta optimización, es solo la basura
errno
setter que es un problema aquí).
-fno-trapping-math
no asume ningún NaN ni nada, solo supone que las excepciones de FP como Invalid o Inexact nunca invocarán un controlador de señal en lugar de producir NaN o un resultado redondeado.
-ftrapping-math
es el valor predeterminado pero
está roto y "nunca funcionó" según el desarrollador de GCC Marc Glisse
.
(Incluso con esto encendido, GCC realiza algunas optimizaciones que pueden cambiar el número de excepciones que se aumentarían de cero a no cero o viceversa. Y bloquea algunas optimizaciones seguras).
Pero desafortunadamente,
https://gcc.gnu.org/bugzilla/show_bug.cgi?id=54192
(activar de forma predeterminada) todavía está abierto.
Si alguna vez desenmascaraste excepciones, podría ser mejor tener
-ftrapping-math
, pero de nuevo es muy raro que alguna vez quieras eso en lugar de solo marcar banderas después de algunas operaciones matemáticas o verificar NaN.
Y de todos modos no conserva la semántica de excepción exacta.
Vea
SIMD para la operación de umbral flotante
para un caso donde
-fno-trapping-math
bloquea incorrectamente una optimización segura.
(¡Incluso después de izar una operación de captura potencial para que la C lo haga incondicionalmente, gcc crea un asm no vectorizado que lo hace condicionalmente! Así que no solo bloquea la vectorización, sino que cambia la semántica de excepción frente a la máquina abstracta C).
Sin ninguna opción global, aquí hay una forma (de bajo costo, pero no gratuita) para obtener una raíz cuadrada sin ramificación:
#include <immintrin.h>
float test(float x)
{
return _mm_cvtss_f32(_mm_sqrt_ss(_mm_set1_ps(x * x)));
}
(en godbolt )
Como de costumbre, Clang es inteligente sobre sus barajas. GCC y MSVC se quedan atrás en esa área, y no logran evitar la transmisión. MSVC también está haciendo algunos movimientos misteriosos.
Hay otras formas de convertir un flotador en un
__m128
, por ejemplo
_mm_set_ss
.
Para Clang eso no hace ninguna diferencia, para GCC que hace que el código sea un poco más grande y peor (incluyendo un
movss reg, reg
que cuenta como un shuffle en Intel, por lo que ni siquiera ahorra en shuffles).