He estado perfilando algunas de nuestras matemáticas básicas en un Intel Core Duo, y mientras observaba varios enfoques de raíz cuadrada, noté algo extraño: al usar las operaciones escalares SSE, es más rápido tomar una raíz cuadrada recíproca y multiplicarla para obtener el sqrt, que usar el código de operación sqrt nativo.
Lo estoy probando con un bucle algo como:
inline float TestSqrtFunction( float in );
void TestFunc()
{
#define ARRAYSIZE 4096
#define NUMITERS 16386
float flIn[ ARRAYSIZE ]; // filled with random numbers ( 0 .. 2^22 )
float flOut [ ARRAYSIZE ]; // filled with 0 to force fetch into L1 cache
cyclecounter.Start();
for ( int i = 0 ; i < NUMITERS ; ++i )
for ( int j = 0 ; j < ARRAYSIZE ; ++j )
{
flOut[j] = TestSqrtFunction( flIn[j] );
// unrolling this loop makes no difference -- I tested it.
}
cyclecounter.Stop();
printf( "%d loops over %d floats took %.3f milliseconds",
NUMITERS, ARRAYSIZE, cyclecounter.Milliseconds() );
}
Probé esto con algunos cuerpos diferentes para TestSqrtFunction, y tengo algunos tiempos que realmente me están rascando la cabeza. Lo peor de todo fue usar la función sqrt () nativa y dejar que el compilador "inteligente" se "optimice". A 24ns / float, usar el x87 FPU esto fue patéticamente malo:
inline float TestSqrtFunction( float in )
{ return sqrt(in); }
Lo siguiente que intenté fue usar un intrínseco para forzar al compilador a usar el código de operación sqrt escalar de SSE:
inline void SSESqrt( float * restrict pOut, float * restrict pIn )
{
_mm_store_ss( pOut, _mm_sqrt_ss( _mm_load_ss( pIn ) ) );
// compiles to movss, sqrtss, movss
}
Esto fue mejor, a 11,9 ns / flotación. También probé la extraña técnica de aproximación Newton-Raphson de Carmack , que funcionó incluso mejor que el hardware, a 4.3ns / float, aunque con un error de 1 en 2 10 (que es demasiado para mis propósitos).
La sorpresa fue cuando probé la operación SSE para la raíz cuadrada recíproca , y luego usé una multiplicación para obtener la raíz cuadrada (x * 1 / √x = √x). Aunque esto elimina dos operaciones dependientes, que era la solución más rápida, con mucho, en 1.24ns / flotador y una precisión de 2 -14 :
inline void SSESqrt_Recip_Times_X( float * restrict pOut, float * restrict pIn )
{
__m128 in = _mm_load_ss( pIn );
_mm_store_ss( pOut, _mm_mul_ss( in, _mm_rsqrt_ss( in ) ) );
// compiles to movss, movaps, rsqrtss, mulss, movss
}
Mi pregunta es básicamente ¿qué da ? ¿Por qué el código de operación de raíz cuadrada integrado en el hardware de SSE es más lento que sintetizarlo a partir de otras dos operaciones matemáticas?
Estoy seguro de que este es realmente el costo de la operación en sí, porque he verificado:
- Todos los datos caben en la caché y los accesos son secuenciales
- las funciones están en línea
- desenrollar el bucle no hace ninguna diferencia
- Los indicadores del compilador están configurados para la optimización completa (y el ensamblaje es bueno, lo verifiqué)
( editar : stephentyrone señala correctamente que las operaciones en cadenas largas de números deben usar las operaciones empaquetadas SIMD de vectorización, como rsqrtps
, pero la estructura de datos de la matriz aquí es solo para fines de prueba: lo que realmente estoy tratando de medir es el rendimiento escalar para usar en el código que no se puede vectorizar).
fuente
inline float SSESqrt( float restrict fIn ) { float fOut; _mm_store_ss( &fOut, _mm_sqrt_ss( _mm_load_ss( &fIn ) ) ); return fOut; }
. Pero esta es una mala idea porque puede inducir fácilmente un bloqueo de carga-golpe-tienda si la CPU escribe los flotantes en la pila y luego los vuelve a leer inmediatamente, haciendo malabarismos desde el registro vectorial a un registro flotante para el valor de retorno en particular es una mala noticia. Además, los códigos de operación subyacentes de la máquina que representan los intrínsecos SSE toman operandos de dirección de todos modos.eax
) es muy malo, mientras que un viaje de ida y vuelta entre xmm0 y la pila y la espalda no lo es, debido al reenvío de tienda de Intel. Puedes cronometrarlo tú mismo para verlo con seguridad. Generalmente, la forma más fácil de ver el LHS potencial es mirar el ensamblaje emitido y ver dónde se combinan los datos entre los conjuntos de registros; su compilador puede hacer algo inteligente, o puede que no. En cuanto a la normalización de vectores, escribí mis resultados aquí: bit.ly/9W5zoURespuestas:
sqrtss
da un resultado correctamente redondeado.rsqrtss
da una aproximación al recíproco, con una precisión de aproximadamente 11 bits.sqrtss
está generando un resultado mucho más preciso, para cuando se requiere precisión.rsqrtss
existe para los casos en que una aproximación es suficiente, pero se requiere velocidad. Si lee la documentación de Intel, también encontrará una secuencia de instrucciones (aproximación recíproca de raíz cuadrada seguida de un solo paso de Newton-Raphson) que brinda una precisión casi total (~ 23 bits de precisión, si mal no recuerdo), y todavía es algo más rápido quesqrtss
.editar: Si la velocidad es crítica, y realmente está llamando a esto en un bucle para muchos valores, debería usar las versiones vectorizadas de estas instrucciones,
rsqrtps
osqrtps
ambas procesan cuatro flotantes por instrucción.fuente
sqrtss
se redondea correctamente , lo que requiere ~ 50 bits antes del redondeo, y no se puede lograr utilizando una iteración N / R simple con precisión simple.Esto también es válido para la división. MULSS (a, RCPSS (b)) es mucho más rápido que DIVSS (a, b). De hecho, es aún más rápido incluso cuando aumenta su precisión con una iteración Newton-Raphson.
Tanto Intel como AMD recomiendan esta técnica en sus manuales de optimización. En aplicaciones que no requieren el cumplimiento de IEEE-754, la única razón para usar div / sqrt es la legibilidad del código.
fuente
div
no es la única operación, por lo que el rendimiento total de uop suele ser el cuello de botella incluso cuando hay undivps
odivss
. Consulte División de punto flotante frente a multiplicación de punto flotante , donde mi respuesta tiene una sección sobre por quércpps
ya no es una ganancia de rendimiento. (O una latencia ganadora) y números en dividir rendimiento / latencia.a * rcpss(b)
puede ser más rápido, ¡pero aún así es más uops quea/b
!En lugar de proporcionar una respuesta, eso en realidad podría ser incorrecto (tampoco voy a verificar ni discutir sobre la caché y otras cosas, digamos que son idénticas), intentaré señalarle la fuente que puede responder a su pregunta.
La diferencia podría estar en cómo se calculan sqrt y rsqrt. Puede leer más aquí http://www.intel.com/products/processor/manuals/ . Sugeriría comenzar leyendo sobre las funciones del procesador que está usando, hay algo de información, especialmente sobre rsqrt (la CPU está usando una tabla de búsqueda interna con una gran aproximación, lo que hace que sea mucho más simple obtener el resultado). Puede parecer que rsqrt es mucho más rápido que sqrt, que 1 operación mul adicional (que no es demasiado costosa) podría no cambiar la situación aquí.
Editar: Algunos hechos que valdría la pena mencionar:
1. Una vez estaba haciendo algunas microoptimizaciones para mi biblioteca de gráficos y usé rsqrt para calcular la longitud de los vectores. (en lugar de sqrt, he multiplicado mi suma de cuadrados por rsqrt, que es exactamente lo que has hecho en tus pruebas), y funcionó mejor.
2. Calcular rsqrt usando una tabla de búsqueda simple podría ser más fácil, como para rsqrt, cuando x va al infinito, 1 / sqrt (x) va a 0, por lo que para las x pequeñas los valores de la función no cambian (mucho), mientras que para sqrt: va al infinito, por lo que es un caso simple;).
Además, una aclaración: no estoy seguro de dónde lo encontré en los libros que he vinculado, pero estoy bastante seguro de haber leído que rsqrt está usando una tabla de búsqueda, y solo debe usarse cuando el resultado no es necesario que sea exacto, aunque yo también podría estar equivocado, como lo fue hace algún tiempo :)
fuente
Newton-Raphson converge al cero de
f(x)
usar incrementos iguales a-f/f'
dondef'
está la derivada.Para
x=sqrt(y)
, se puede tratar de resolverf(x) = 0
porx
el usof(x) = x^2 - y
;Entonces el incremento es:
dx = -f/f' = 1/2 (x - y/x) = 1/2 (x^2 - y) / x
que tiene una división lenta.Puedes probar otras funciones (como
f(x) = 1/y - 1/x^2
) pero serán igualmente complicadas.Veamos
1/sqrt(y)
ahora. Puedes intentarlof(x) = x^2 - 1/y
, pero será igualmente complicado:dx = 2xy / (y*x^2 - 1)
por ejemplo. Una opción alternativa no obvia paraf(x)
es:f(x) = y - 1/x^2
Luego:
dx = -f/f' = (y - 1/x^2) / (2/x^3) = 1/2 * x * (1 - y * x^2)
¡Ah! No es una expresión trivial, pero solo tiene multiplicaciones, no divide. => ¡Más rápido!
Y: el paso de actualización completo
new_x = x + dx
luego dice:x *= 3/2 - y/2 * x * x
que también es fácil.fuente
Hay varias otras respuestas a esto ya desde hace unos años. Esto es lo que acertó el consenso:
Esto es lo que se equivocó en el consenso:
El algoritmo NR para calcular la raíz cuadrada recíproca tiene este paso de actualización, como otros han señalado:
Eso es un montón de multiplicaciones dependientes de datos y una resta.
Lo que sigue es el algoritmo que utilizan realmente las FPU modernas.
Dado
b[0] = n
, suponga que podemos encontrar una serie de númerosY[i]
tal que seb[n] = b[0] * Y[0]^2 * Y[1]^2 * ... * Y[n]^2
acerque a 1. Luego considere:Es evidente que
x[n]
se acercasqrt(n)
yy[n]
se acerca1/sqrt(n)
.Podemos usar el paso de actualización de Newton-Raphson para la raíz cuadrada recíproca para obtener un buen
Y[i]
:Luego:
y:
La siguiente observación clave es esa
b[i] = x[i-1] * y[i-1]
. Entonces:Luego:
Es decir, dados xey iniciales, podemos usar el siguiente paso de actualización:
O, incluso más elegante, podemos configurar
h = 0.5 * y
. Esta es la inicialización:Y este es el paso de actualización:
Este es el algoritmo de Goldschmidt, y tiene una gran ventaja si lo está implementando en hardware: el "bucle interno" es tres adiciones múltiples y nada más, y dos de ellos son independientes y se pueden canalizar.
En 1999, las FPU ya necesitaban un circuito de suma / resta en canalización y un circuito de multiplicación en canalización; de lo contrario, SSE no sería muy "fluido". En 1999, solo se necesitaba uno de cada circuito para implementar este bucle interno de una manera completamente canalizada sin desperdiciar mucho hardware solo en la raíz cuadrada.
Hoy, por supuesto, hemos fusionado la adición múltiple expuesta al programador. Una vez más, el bucle interno son tres FMA canalizados, que son (nuevamente) generalmente útiles incluso si no está calculando raíces cuadradas.
fuente
_mm256_rsqrt_ps
, con análisis de perf de Haswell. Por lo general, solo es una buena idea si no tiene otro trabajo en el bucle y se produciría un cuello de botella en el rendimiento del divisor. HW sqrt es uop único, por lo que está bien mezclado con otro trabajo.Es más rápido porque estas instrucciones ignoran los modos de redondeo y no manejan excepciones de punto flotante o números desnormalizados. Por estas razones, es mucho más fácil canalizar, especular y ejecutar otras instrucciones fp fuera de servicio.
fuente
rsqrt
la precisión mucho menor, lo que significa mucho menos trabajo por hacer (¿o nada?) Después de una búsqueda en la tabla para obtener una suposición inicial.