Wie funktioniert diese Float-Quadratwurzel-Näherung?

Lesezeit: 14 Minuten

Benutzeravatar von YSC
YSC

Ich fand eine ziemlich seltsame, aber funktionierende Quadratwurzel-Näherung für floats; Ich verstehe es wirklich nicht. Kann mir jemand erklären warum dieser Code funktioniert?

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

Ich habe es ein bisschen getestet und es gibt Werte von aus std::sqrt() um etwa 1 bis 3 %. Ich kenne die Quake III schnelle inverse Quadratwurzel und ich denke, es ist hier etwas Ähnliches (ohne die Newton-Iteration), aber ich würde mich sehr über eine Erklärung freuen wie es funktioniert.

(Anmerkung: Ich habe es sowohl mit c als auch mit c++ gekennzeichnet, da es sich um gültigen (siehe Kommentare) C- und C++-Code handelt.)

  • Es ist weder gültiges C noch gültiges C++. Es verstößt gegen Aliasing-Regeln und nimmt eine bestimmte Darstellung für Fließkommawerte und für an int Werte. Das macht es zu einem Hackerhead-Code, der manchmal faszinierend ist, aber im Allgemeinen nicht nachgeahmt werden sollte.

    – Peter Becker

    30. März 2017 um 14:03 Uhr

  • Dies ist eine Art Freund des andere magische Zahl 0x5f3759df

    – Eugen Sch.

    30. März 2017 um 14:03 Uhr

  • Ungefähr gesagt, Rechtsverschiebung der bitweisen Darstellung von f dividiert den Exponenten durch zwei, was dem Ziehen der Quadratwurzel entspricht. Alles andere ist vermutlich magisch, um die Genauigkeit über den Mantissenbereich zu verbessern.

    – Oliver Charlesworth

    30. März 2017 um 14:06 Uhr

  • dividiert den Exponenten durch zwei, was dem Ziehen der Quadratwurzel entspricht was

    – Fureisch

    30. März 2017 um 14:10 Uhr

  • @Fureeish – sqrt (a ^ b) = (a ^ b) ^ 0,5 = a ^ (b / 2).

    – Oliver Charlesworth

    30. März 2017 um 14:11 Uhr

Benutzeravatar von Oliver Charlesworth
Oliver Charlesworth

(*(int*)&f >> 1) verschiebt die bitweise Darstellung von nach rechts f. Dies fast dividiert den Exponenten durch zwei, was ungefähr dem Ziehen der Quadratwurzel entspricht.1

Warum fast? In IEEE-754 ist der eigentliche Exponent e-127.2 Um dies durch zwei zu teilen, bräuchten wir e/2 – 64aber die obige Annäherung gibt uns nur e/2 – 127. Also müssen wir 63 zum resultierenden Exponenten addieren. Dies wird durch die Bits 30-23 dieser magischen Konstante (0x1fbb4000).

Ich würde mir vorstellen, dass die verbleibenden Bits der magischen Konstante ausgewählt wurden, um den maximalen Fehler über den Mantissenbereich oder so ähnlich zu minimieren. Es ist jedoch unklar, ob es analytisch, iterativ oder heuristisch bestimmt wurde.


Es ist erwähnenswert, dass dieser Ansatz etwas nicht portabel ist. Sie macht (mindestens) folgende Annahmen:

  • Die Plattform verwendet IEEE-754 mit einfacher Genauigkeit für float.
  • Die Endianität von float Darstellung.
  • Dass Sie von undefiniertem Verhalten nicht betroffen sind, da dieser Ansatz gegen die strikten Aliasing-Regeln von C/C++ verstößt.

Daher sollte es vermieden werden, es sei denn, Sie sind sich sicher, dass es ein vorhersehbares Verhalten auf Ihrer Plattform liefert (und tatsächlich eine nützliche Beschleunigung gegenüber sqrtf!).


1. sqrt(a^b) = (a^b)^0,5 = a^(b/2)

2. Siehe zB https://en.wikipedia.org/wiki/Single-precision_floating-point_format#Exponent_encoding

  • Es kann auch eine Folge davon sein Mathe Kobolde 🙂

    – Sembei Norimaki

    30. März 2017 um 14:23 Uhr


  • IMO, die Wahrscheinlichkeit, diese “Nicht-Portabilitäten” zu treffen, liegt nahe bei Null. IEEE-754 wird allgemein angenommen, Maschinen mit unterschiedlicher Integer- und Fließkomma-Endianness sind (waren) Ausnahmen.

    – Yves Daust

    30. März 2017 um 15:04 Uhr

  • Gute Antwort. Detail: “Die Endianness der Float-Darstellung.” ist relativ zur Endianness des int. Wenn sie beide groß oder beide klein sind, dann ist Endianness kein Problem.

    – chux – Wiedereinsetzung von Monica

    30. März 2017 um 15:29 Uhr

  • Dies tut verstoßen zweifellos gegen die strenge Aliasing-Regel, sowohl in C als auch in C++. “Dass Sie nicht gegen die strikten Aliasing-Regeln von C/C++ verstoßen.” schlägt vor, dass es kann oder nicht. Es ist bekannt, dass moderne Compiler TBAA aggressiv ausführen, die Spur der Geschichte ist übersät mit den Kadavern von Leuten, die dachten, „die Wahrscheinlichkeit, diese Nicht-Portabilitäten zu treffen, ist nahe Null“. Ich würde gerne sehen, dass die Antwort eindeutig besagt, dass sie gegen die Regel verstößt, und OP sollte entweder den Code ändern oder den Compiler mit deaktiviertem TBAA aufrufen (gcc und clang haben dafür einen Schalter).

    – MM

    31. März 2017 um 0:32 Uhr


  • Ein weiteres noch nicht erwähntes Portabilitätsproblem ist die Rechtsverschiebung eines Negativs int erzeugt einen implementierungsdefinierten Wert

    – MM

    31. März 2017 um 0:39 Uhr

Benutzeravatar von Davislor
Davislor

Siehe Oliver Charlesworths Erklärung, warum das so ist fast funktioniert. Ich spreche ein Problem an, das in den Kommentaren angesprochen wurde.

Da mehrere Leute darauf hingewiesen haben, dass dies nicht portierbar ist, finden Sie hier einige Möglichkeiten, wie Sie es portierbarer machen oder zumindest den Compiler dazu bringen können, Ihnen mitzuteilen, ob es nicht funktioniert.

Erstens erlaubt Ihnen C++, dies zu überprüfen std::numeric_limits<float>::is_iec559 zur Kompilierzeit, wie in a static_assert. Das kannst du auch überprüfen sizeof(int) == sizeof(float)was nicht wahr ist, wenn int ist 64-Bit, aber was Sie wirklich tun möchten, ist zu verwenden uint32_t, die, wenn sie existiert, immer genau 32 Bit breit ist, ein wohldefiniertes Verhalten bei Verschiebungen und Überläufen hat und einen Kompilierungsfehler verursacht, wenn Ihre seltsame Architektur keinen solchen integralen Typ hat. So oder so, sollten Sie auch static_assert() dass die Typen die gleiche Größe haben. Statische Zusicherungen haben keine Laufzeitkosten und Sie sollten Ihre Vorbedingungen nach Möglichkeit immer auf diese Weise überprüfen.

Leider ist der Test, ob die Konvertierung der Bits in a float zu einem uint32_t und Shifting Big-Endian, Little-Endian oder keines von beiden ist, kann nicht als konstanter Ausdruck zur Kompilierzeit berechnet werden. Hier habe ich die Laufzeitprüfung in den Teil des Codes eingefügt, der davon abhängt, aber Sie möchten sie vielleicht in die Initialisierung einfügen und nur einmal ausführen. In der Praxis können sowohl gcc als auch clang diesen Test zur Kompilierzeit optimieren.

Sie möchten die unsichere Zeigerumwandlung nicht verwenden, und es gibt einige Systeme, an denen ich in der realen Welt gearbeitet habe, bei denen dies das Programm mit einem Busfehler zum Absturz bringen könnte. Die maximal tragbare Methode zum Konvertieren von Objektdarstellungen ist with memcpy(). In meinem Beispiel unten schreibe ich ein Wortspiel mit a union, die auf jeder tatsächlich existierenden Implementierung funktioniert. (Sprachanwälte lehnen dies ab, aber kein erfolgreicher Compiler wird jemals so viel Legacy-Code brechen schweigend.) Wenn Sie eine Zeigerkonvertierung durchführen müssen (siehe unten), gibt es eine alignas(). Aber wie auch immer Sie es tun, das Ergebnis wird implementierungsabhängig sein, weshalb wir das Ergebnis der Konvertierung und Verschiebung eines Testwerts überprüfen.

Wie auch immer, nicht dass Sie es wahrscheinlich auf einer modernen CPU verwenden werden, hier ist eine verspielte C++ 14-Version, die diese nicht-portablen Annahmen überprüft:

#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;
}

Aktualisieren

Hier ist eine alternative Definition von reinterpret<T,U>() das vermeidet Wortspielereien. Sie könnten das Typ-Wortspiel auch in modernem C implementieren, wo es standardmäßig erlaubt ist, und die Funktion als aufrufen extern "C". Ich denke, Typ-Wortspiel ist eleganter, typsicherer und im Einklang mit dem quasi-funktionalen Stil dieses Programms als memcpy(). Ich glaube auch nicht, dass Sie viel gewinnen, weil Sie immer noch ein undefiniertes Verhalten von einer hypothetischen Fallendarstellung haben könnten. Außerdem ist clang++ 3.9.1 -O -S in der Lage, die Type-Puning-Version statisch zu analysieren und die Variable zu optimieren is_little_endian zur Konstante 0x1und den Laufzeittest eliminieren, aber er kann diese Version nur bis zu einem Stub mit einer einzigen Anweisung optimieren.

Aber was noch wichtiger ist, es ist nicht garantiert, dass dieser Code auf jedem Compiler portabel funktioniert. Beispielsweise können einige alte Computer nicht einmal genau 32 Bit Speicher adressieren. Aber in diesen Fällen sollte es nicht kompilieren und Ihnen sagen, warum. Kein Compiler wird einfach ohne Grund plötzlich eine riesige Menge an Legacy-Code brechen. Obwohl der Standard dies technisch erlaubt und immer noch sagt, dass er C++14 entspricht, wird dies nur auf einer Architektur geschehen, die sich stark von der erwarteten unterscheidet. Und wenn unsere Annahmen so ungültig sind, dass ein Compiler ein Wortspiel zwischen a umwandelt float und eine 32-Bit-Ganzzahl ohne Vorzeichen in einen gefährlichen Fehler, bezweifle ich wirklich, dass die Logik hinter diesem Code bestehen bleibt, wenn wir nur verwenden memcpy() stattdessen. Wir möchten, dass dieser Code zur Kompilierzeit fehlschlägt und uns mitteilt, warum.

#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;

Stroustrup et al., in der C++ Core-Richtlinienempfehle a reinterpret_cast stattdessen:

#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);
}

Die von mir getesteten Compiler können dies auch zu einer gefalteten Konstante optimieren. Stroustrups Argumentation ist [sic]:

Zugriff auf das Ergebnis einer reinterpret_cast auf einen anderen Typ als den deklarierten Typ des Objekts zu ändern, ist immer noch ein undefiniertes Verhalten, aber zumindest können wir sehen, dass etwas Verzwicktes vor sich geht.

Aktualisieren

Aus den Kommentaren: C++20 führt ein std::bit_castdie eine Objektdarstellung in einen anderen Typ mit konvertiert nicht spezifiziertnicht nicht definiert, Verhalten. Dies garantiert nicht, dass Ihre Implementierung dasselbe Format verwendet float und int das dieser Code erwartet, aber es gibt dem Compiler keinen Freibrief, Ihr Programm willkürlich zu unterbrechen, weil es technisch undefiniertes Verhalten in einer Zeile davon gibt. Es kann Ihnen auch eine geben constexpr Wandlung.

  • In C ++ ist es ein undefiniertes Verhalten, ein anderes Mitglied einer Union als das zuletzt geschriebene zu lesen (siehe diese Antwort, insbesondere den letzten Absatz).

    – MM

    31. März 2017 um 0:36 Uhr

  • Habe diesen Absatz bearbeitet. Ja, es ist offiziell eine Spracherweiterung, die zufälligerweise jeder größere Compiler unterstützt. Wenn Sie wirklich IDB statt UB wollen, verwenden Sie memcpy(). Sie riskieren immer noch, eine Fallenvertretung zu bekommen. Ich denke, der Code, den ich geschrieben habe, ist sicherer und eleganter als memcpy(), obwohl. Es ist typsicher, es handelt sich um rein funktionalen Code, bei dem keine Variable geändert wird, und es kann statisch analysiert werden (sogar konstant gefaltet). Später wird separat überprüft, ob die Ergebnisse unseren Erwartungen entsprechen. Und wenn Wortspiele verboten sind, wird uns in Zukunft jeder vernünftige Compiler einen Kompilierzeitfehler ausgeben.

    – Davislor

    31. März 2017 um 1:45 Uhr


  • @MM Eine Version hinzugefügt, die verwendet memcpy() und eine Erläuterung des von Ihnen angesprochenen Problems.

    – Davislor

    31. März 2017 um 6:17 Uhr

  • @Davislor: In Bezug auf das Gewerkschaftsproblem möchte ich anmerken, dass Undefined Behaviour der Implementierung die Freiheit lässt, zu tun, was sie will. Die Implementierung, die garantiert, dass Typ-Punning über Unions funktioniert, liegt innerhalb des Perimeters von “was auch immer sie wollen”, und daher, wenn Sie eine Garantie für Ihre Compiler von Interesse haben, können Sie loslegen.

    – Matthias M.

    31. März 2017 um 8:12 Uhr

  • @MatthieuM, Es ist eine kontroverse Meinung, und die C++-Kernrichtlinien von Stroustrup empfehlen die Dereferenzierung von a reinterpret_cast stattdessen auf einem Zeiger. (Ich habe meine Antwort bearbeitet, um ein Zitat und ein Beispiel zu geben.) Aber im Grunde wird kein Compiler die Typ-Punning-Sprache ohne Grund brechen, in einem Fall, der so trivial ist wie das Wortspiel zwischen einem arithmetischen Typ und einer vorzeichenlosen Ganzzahl gleicher Größe . Wenn das nicht das tut, was wir erwarten, stimmen unsere Grundannahmen über die Architektur nicht. Und in diesem Fall möchte ich, dass der Compiler dies zur Kompilierzeit als logischen Fehler kennzeichnet.

    – Davislor

    31. März 2017 um 9:04 Uhr


Benutzeravatar von Michael Foukarakis
Michael Foukarakis

Sei y = sqrt(x),

aus den Eigenschaften von Logarithmen folgt log(y) = 0,5 * log(x) (1)

Interpretieren eines Normalen float als ganze Zahl ergibt INT(x) = Ix = L * (log(x) + B – σ) (2)

wobei L = 2 ^ N, N die Anzahl der Bits des Signifikanten, B die Exponentenabweichung und σ ein freier Faktor zum Abstimmen der Annäherung ist.

Die Kombination von (1) und (2) ergibt: Iy = 0,5 * (Ix + (L * (B – σ)))

Was im Code so geschrieben ist (*(int*)&x >> 1) + 0x1fbb4000;

Finden Sie das σ so, dass die Konstante gleich 0x1fbb4000 ist, und bestimmen Sie, ob es optimal ist.

  • Beachten Sie das mit Commonfloatdas MSbit des Signifikanten wird nicht codiert, sondern nur als 1 für normal angenommen float. Dies betrifft OPs float sqrt(float f) noch nicht berücksichtigt INT(x)

    – chux – Wiedereinsetzung von Monica

    30. März 2017 um 16:10 Uhr


  • Ja, wie Sie in Ihrem Beitrag angemerkt haben, ist diese Annäherung nur für den Normalzustand genau floats.

    – Michael Foukarakis

    30. März 2017 um 16:17 Uhr

Hinzufügen eines Wiki-Testrahmens, um alles zu testen float.

Die Annäherung liegt für viele innerhalb von 4% floataber sehr schlecht für subnormale Zahlen. YMMV

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

Beachten Sie, dass bei einem Argument von +/-0,0 das Ergebnis nicht Null ist.

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

Code testen

#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;
}

1410880cookie-checkWie funktioniert diese Float-Quadratwurzel-Näherung?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy