c++ gcc assembly floating-point micro-optimization

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).