tutorial standard simple sacar resueltos regla raiz raices que punto programacion programa precisión potencia paso numeros norma matematica infinito hexadecimal flotantes flotante facilmente exceso estándar elevar ejercicios ejemplos doble decimales cubica cuadrado cuadrada con como binario c++ c optimization floating-point ieee-754

c++ - standard - ¿Cómo funciona esta aproximación de raíz cuadrada flotante?



standard 754 ieee (4)

Encontré una aproximación de raíz cuadrada bastante extraña pero funcional para float s; Realmente no lo entiendo. ¿Alguien puede explicarme por qué funciona este código?

float sqrt(float f) { const int result = 0x1fbb4000 + (*(int*)&f >> 1); return *(float*)&result; }

Lo probé un poco y genera valores fuera de std::sqrt() en aproximadamente 1 a 3% . Sé de la raíz cuadrada inversa rápida del Quake III y supongo que es algo similar aquí (sin la iteración de Newton), pero realmente agradecería una explicación de cómo funciona .

(nota: lo he etiquetado como c y c ++ ya que es válido-ish (ver comentarios) Código C y C ++)


Agregar un arnés de prueba wiki para probar todo el float .

La aproximación está dentro del 4% para muchos float , pero muy pobre para los números por debajo de lo normal. YMMV

Worst:1.401298e-45 211749.20% Average:0.63% Worst:1.262738e-38 3.52% Average:0.02%

Tenga en cuenta que con un argumento de +/- 0.0, el resultado no es cero.

printf("% e % e/n", sqrtf(+0.0), sqrt_apx(0.0)); // 0.000000e+00 7.930346e-20 printf("% e % e/n", sqrtf(-0.0), sqrt_apx(-0.0)); // -0.000000e+00 -2.698557e+19

Código de prueba

#include <float.h> #include <limits.h> #include <math.h> #include <stddef.h> #include <stdio.h> #include <stdint.h> #include <stdlib.h> float sqrt_apx(float f) { const int result = 0x1fbb4000 + (*(int*) &f >> 1); return *(float*) &result; } double error_value = 0.0; double error_worst = 0.0; double error_sum = 0.0; unsigned long error_count = 0; void sqrt_test(float f) { if (f == 0) return; volatile float y0 = sqrtf(f); volatile float y1 = sqrt_apx(f); double error = (1.0 * y1 - y0) / y0; error = fabs(error); if (error > error_worst) { error_worst = error; error_value = f; } error_sum += error; error_count++; } void sqrt_tests(float f0, float f1) { error_value = error_worst = error_sum = 0.0; error_count = 0; for (;;) { sqrt_test(f0); if (f0 == f1) break; f0 = nextafterf(f0, f1); } printf("Worst:%e %.2f%%/n", error_value, error_worst*100.0); printf("Average:%.2f%%/n", error_sum / error_count); fflush(stdout); } int main() { sqrt_tests(FLT_TRUE_MIN, FLT_MIN); sqrt_tests(FLT_MIN, FLT_MAX); return 0; }


Deje y = sqrt (x),

De las propiedades de los logaritmos se desprende que log (y) = 0.5 * log (x) (1)

Interpretar un float normal como un entero da INT (x) = Ix = L * (log (x) + B - σ) (2)

donde L = 2 ^ N, N el número de bits del significado, B es el sesgo del exponente, y σ es un factor libre para ajustar la aproximación.

La combinación de (1) y (2) da: Iy = 0.5 * (Ix + (L * (B - σ)))

Que está escrito en el código como (*(int*)&x >> 1) + 0x1fbb4000;

Encuentre el σ para que la constante sea igual a 0x1fbb4000 y determine si es óptima.


Vea la explicación de Oliver Charlesworth de por qué esto casi funciona. Estoy abordando un problema planteado en los comentarios.

Dado que varias personas han señalado la no portabilidad de esto, aquí hay algunas formas en que puede hacerlo más portátil, o al menos hacer que el compilador le diga si no funcionará.

Primero, C ++ le permite verificar std::numeric_limits<float>::is_iec559 en tiempo de compilación, como en un static_assert . También puede verificar que sizeof(int) == sizeof(float) , que no será cierto si int es de 64 bits, pero lo que realmente quiere hacer es usar uint32_t , que si existe siempre tendrá exactamente 32 bits de ancho , tendrá un comportamiento bien definido con cambios y desbordamiento, y provocará un error de compilación si su arquitectura extraña no tiene ese tipo integral. De cualquier manera, también debe static_assert() que los tipos tengan el mismo tamaño. Las aserciones estáticas no tienen costo de tiempo de ejecución y siempre debe verificar sus condiciones previas de esta manera si es posible.

Desafortunadamente, la prueba de si la conversión de los bits en un float a uint32_t y el desplazamiento es big-endian, little-endian o ninguno de los dos puede calcularse como una expresión constante en tiempo de compilación. Aquí, puse la verificación de tiempo de ejecución en la parte del código que depende de él, pero es posible que desee ponerlo en la inicialización y hacerlo una vez. En la práctica, tanto gcc como clang pueden optimizar esta prueba en tiempo de compilación.

No desea utilizar el puntero inseguro, y hay algunos sistemas en los que he trabajado en el mundo real en los que podría bloquear el programa con un error de bus. La forma más portátil de convertir representaciones de objetos es con memcpy() . En mi ejemplo a continuación, escribo juegos de palabras con una union , que funciona en cualquier implementación existente. (Los abogados de idiomas se oponen, pero ningún compilador exitoso romperá ese código heredado en silencio .) Si debe hacer una conversión de puntero (ver más abajo) hay alignas() . Pero, independientemente de cómo lo haga, el resultado estará definido por la implementación, por lo que verificamos el resultado de convertir y cambiar un valor de prueba.

De todos modos, no es probable que lo use en una CPU moderna, aquí hay una versión de C ++ 14 que verifica esos supuestos no portátiles:

#include <cassert> #include <cmath> #include <cstdint> #include <cstdlib> #include <iomanip> #include <iostream> #include <limits> #include <vector> using std::cout; using std::endl; using std::size_t; using std::sqrt; using std::uint32_t; template <typename T, typename U> inline T reinterpret(const U x) /* Reinterprets the bits of x as a T. Cannot be constexpr * in C++14 because it reads an inactive union member. */ { static_assert( sizeof(T)==sizeof(U), "" ); union tu_pun { U u = U(); T t; }; const tu_pun pun{x}; return pun.t; } constexpr float source = -0.1F; constexpr uint32_t target = 0x5ee66666UL; const uint32_t after_rshift = reinterpret<uint32_t,float>(source) >> 1U; const bool is_little_endian = after_rshift == target; float est_sqrt(const float x) /* A fast approximation of sqrt(x) that works less well for subnormal numbers. */ { static_assert( std::numeric_limits<float>::is_iec559, "" ); assert(is_little_endian); // Could provide alternative big-endian code. /* The algorithm relies on the bit representation of normal IEEE floats, so * a subnormal number as input might be considered a domain error as well? */ if ( std::isless(x, 0.0F) || !std::isfinite(x) ) return std::numeric_limits<float>::signaling_NaN(); constexpr uint32_t magic_number = 0x1fbb4000UL; const uint32_t raw_bits = reinterpret<uint32_t,float>(x); const uint32_t rejiggered_bits = (raw_bits >> 1U) + magic_number; return reinterpret<float,uint32_t>(rejiggered_bits); } int main(void) { static const std::vector<float> test_values{ 4.0F, 0.01F, 0.0F, 5e20F, 5e-20F, 1.262738e-38F }; for ( const float& x : test_values ) { const double gold_standard = sqrt((double)x); const double estimate = est_sqrt(x); const double error = estimate - gold_standard; cout << "The error for (" << estimate << " - " << gold_standard << ") is " << error; if ( gold_standard != 0.0 && std::isfinite(gold_standard) ) { const double error_pct = error/gold_standard * 100.0; cout << " (" << error_pct << "%)."; } else cout << ''.''; cout << endl; } return EXIT_SUCCESS; }

Actualizar

Aquí hay una definición alternativa de reinterpret<T,U>() que evita la escritura de tipos. También puede implementar el juego de palabras en C moderno, donde está permitido por estándar, y llamar a la función como extern "C" . Creo que la memcpy() es más elegante, segura y consistente con el estilo casi funcional de este programa que memcpy() . Tampoco creo que ganes mucho, porque aún podrías tener un comportamiento indefinido de una representación hipotética de trampa. Además, clang ++ 3.9.1 -O -S es capaz de analizar estáticamente la versión de is_little_endian , optimizar la variable is_little_endian a la constante 0x1 y eliminar la prueba de tiempo de ejecución, pero solo puede optimizar esta versión a una sola- trozo de instrucciones.

Pero lo que es más importante, no se garantiza que este código funcione de manera portátil en cada compilador. Por ejemplo, algunas computadoras antiguas ni siquiera pueden direccionar exactamente 32 bits de memoria. Pero en esos casos, no debería compilarse y decirte por qué. Ningún compilador de repente va a romper una gran cantidad de código heredado sin ninguna razón. Aunque el estándar técnicamente da permiso para hacerlo y aún dice que se ajusta a C ++ 14, solo sucederá en una arquitectura muy diferente de lo que esperamos. Y si nuestras suposiciones son tan inválidas que algún compilador va a convertir un juego de palabras entre un float y un entero sin signo de 32 bits en un error peligroso, dudo mucho que la lógica detrás de este código se mantenga si usamos memcpy() lugar. Queremos que ese código falle en el momento de la compilación y que nos diga por qué.

#include <cassert> #include <cstdint> #include <cstring> using std::memcpy; using std::uint32_t; template <typename T, typename U> inline T reinterpret(const U &x) /* Reinterprets the bits of x as a T. Cannot be constexpr * in C++14 because it modifies a variable. */ { static_assert( sizeof(T)==sizeof(U), "" ); T temp; memcpy( &temp, &x, sizeof(T) ); return temp; } constexpr float source = -0.1F; constexpr uint32_t target = 0x5ee66666UL; const uint32_t after_rshift = reinterpret<uint32_t,float>(source) >> 1U; extern const bool is_little_endian = after_rshift == target;

Sin embargo, Stroustrup et al., En las Pautas principales de C ++ , recomiendan un reinterpret_cast lugar:

#include <cassert> template <typename T, typename U> inline T reinterpret(const U x) /* Reinterprets the bits of x as a T. Cannot be constexpr * in C++14 because it uses reinterpret_cast. */ { static_assert( sizeof(T)==sizeof(U), "" ); const U temp alignas(T) alignas(U) = x; return *reinterpret_cast<const T*>(&temp); }

Los compiladores que probé también pueden optimizar esto a una constante doblada. El razonamiento de Stroustrup es [sic]:

Acceder al resultado de un reinterpret_cast a un tipo diferente de los objetos declarados tipo sigue siendo un comportamiento indefinido, pero al menos podemos ver que algo complicado está sucediendo.


(*(int*)&f >> 1) desplaza a la derecha la representación bit a bit de f . Esto casi divide el exponente por dos, lo que es aproximadamente equivalente a sacar la raíz cuadrada. 1

¿Por qué casi ? En IEEE-754, el exponente real es e - 127 . 2 Para dividir esto entre dos, necesitaríamos e / 2 - 64 , pero la aproximación anterior solo nos da e / 2 - 127 . Por lo tanto, necesitamos agregar 63 al exponente resultante. Esto es contribuido por los bits 30-23 de esa constante mágica ( 0x1fbb4000 ).

Me imagino que los bits restantes de la constante mágica se han elegido para minimizar el error máximo en todo el rango de mantisa, o algo así. Sin embargo, no está claro si se determinó de forma analítica, iterativa o heurística.

Vale la pena señalar que este enfoque es algo no portátil. Hace (al menos) los siguientes supuestos:

  • La plataforma utiliza IEEE-754 de precisión simple para float .
  • La endianidad de la representación float .
  • Que no se verá afectado por un comportamiento indefinido debido al hecho de que este enfoque viola las reglas de alias estricto de C / C ++.

Por lo tanto, debe evitarse a menos que esté seguro de que proporciona un comportamiento predecible en su plataforma (y de hecho, proporciona una aceleración útil frente a sqrtf ).

1. sqrt (a ^ b) = (a ^ b) ^ 0.5 = a ^ (b / 2)

2. Ver, por ejemplo, https://en.wikipedia.org/wiki/Single-precision_floating-point_format#Exponent_encoding