Wie im vorherigen Artikel, Teil 132-145 gezeigt. Wird fĂŒr x im Bereich [0,126, 0,855469] ausgefĂŒhrt. Und was. Versuchen wir, eine Funktion zu schreiben, die innerhalb der vorgegebenen Grenzen genauer und möglicherweise schneller ist.
Die Art und Weise, wie wir es verwenden, ist ziemlich offensichtlich. Die Genauigkeit der Berechnungen muss um weitere Dezimalstellen erweitert werden. Die naheliegende Lösung wĂ€re, den langen Doppeltyp auszuwĂ€hlen, darin zu zĂ€hlen und dann zurĂŒck zu konvertieren. In Bezug auf die Genauigkeit sollte die Lösung gut sein, aber in Bezug auf die Leistung kann es Probleme geben. Long Double ist jedoch eine ziemlich exotische Art von Daten, und ihre UnterstĂŒtzung in modernen Prozessoren hat keine PrioritĂ€t. Unter x86_64 funktionieren SSE / AVX-Anweisungen mit diesem Datentyp nicht. Der mathematische Coprozessor wird "weggeblasen".
Was solltest du dann wÀhlen? Schauen wir uns die Argument- und Funktionsgrenzen genauer an.
Sie befinden sich in der Region 1.0. Jene. TatsĂ€chlich brauchen wir keinen Gleitkomma. Verwenden wir bei der Berechnung der Funktion eine 64-Bit-Ganzzahl. Dies gibt uns zusĂ€tzliche 10-11 Bits zur ursprĂŒnglichen Genauigkeit. Lassen Sie uns herausfinden, wie man mit diesen Zahlen arbeitet. Eine Zahl in diesem Format wird als a / d dargestellt , wobei a eine Ganzzahl und d ein Divisor ist, den wir fĂŒr alle Variablen als Konstante auswĂ€hlen und "in unserem Speicher" und nicht im Speicher des Computers speichern. Nachfolgend sind einige Operationen fĂŒr solche Nummern aufgefĂŒhrt:
Wie Sie sehen, ist daran nichts Kompliziertes. Die letzte Formel zeigt die Multiplikation mit einer beliebigen Ganzzahl. Beachten Sie auch eine ziemlich offensichtliche Sache, dass das Ergebnis der Multiplikation von zwei vorzeichenlosen ganzzahligen Variablen der GröĂe N hĂ€ufiger eine Anzahl von GröĂen bis zu 2 * N einschlieĂlich ist. Das HinzufĂŒgen kann einen Ăberlauf von bis zu 1 zusĂ€tzlichen Bit verursachen.
Lassen Sie uns versuchen , den Divisor wĂ€hlen d . In der binĂ€ren Welt ist es natĂŒrlich am besten, sie als Zweierpotenz zu wĂ€hlen, um nicht zu teilen, sondern nur das Register zu verschieben. Welche Zweierpotenz solltest du wĂ€hlen? Den Hinweis finden Sie in den Anweisungen der Multiplikationsmaschine. Beispielsweise multipliziert der Standard-MUL-Befehl im x86-System 2 Register und schreibt das Ergebnis auch in 2 Register, wobei 1 der Register der "obere Teil" des Ergebnisses und das zweite der untere Teil ist.
Wenn wir beispielsweise zwei 64-Bit-Nummern haben, ist das Ergebnis eine 128-Bit-Nummer, die in zwei 64-Bit-Register geschrieben wird. Nennen wir RH - "GroĂbuchstaben" und RL - "Kleinbuchstaben" 1 . Dann kann das Ergebnis mathematisch wie folgt geschrieben werden . Nun verwenden wir die obigen Formeln und schreiben die Multiplikation fĂŒr
Und es stellt sich heraus, dass das Ergebnis der Multiplikation dieser beiden Festkommazahlen das Register ist .
FĂŒr das Aarch64-System ist es noch einfacher. Der Befehl "UMULH" multipliziert zwei Register und schreibt den "oberen" Teil der Multiplikation in das 3. Register.
Na dann. Wir haben eine Festkommazahl angegeben, aber es gibt immer noch ein Problem. Negative Zahlen. In der Taylor-Reihe geht die Erweiterung mit einem variablen Vorzeichen einher. Um dieses Problem zu lösen, transformieren wir die Formel zur Berechnung des Polynoms nach der Goner-Methode in die folgende Form:
ĂberprĂŒfen Sie, ob es mathematisch genau mit der ursprĂŒnglichen Formel ĂŒbereinstimmt. Aber in jeder Klammer gibt es eine Nummer der Form immer positiv. Jene. Durch diese Konvertierung kann der Ausdruck als vorzeichenlose Ganzzahlen ausgewertet werden.
constexpr mynumber toint = {{0x00000000, 0x43F00000}}; /* 18446744073709551616 = 2^64 */
constexpr mynumber todouble = {{0x00000000, 0x3BF00000}}; /* ~5.42101086242752217003726400434E-20 = 2^-64 */
double sin_e7(double xd) {
uint64_t x = xd * toint.x;
uint64_t xx = mul2(x, x);
uint64_t res = tsx[19];
for(int i = 17; i >= 3; i -= 2) {
res = tsx[i] - mul2(res, xx);
}
res = mul2(res, xx);
res = x - mul2(x, res);
return res * todouble.x;
}
Tsx [i] -Werte
constexpr array<uint64_t, 18> tsx = { // 2^64/i!
0x0000000000000000LL,
0x0000000000000000LL,
0x8000000000000000LL,
0x2aaaaaaaaaaaaaaaLL, // Change to 0x2aaaaaaaaaaaaaafLL and check.
0x0aaaaaaaaaaaaaaaLL,
0x0222222222222222LL,
0x005b05b05b05b05bLL,
0x000d00d00d00d00dLL,
0x0001a01a01a01a01LL,
0x00002e3bc74aad8eLL,
0x0000049f93edde27LL,
0x0000006b99159fd5LL,
0x00000008f76c77fcLL,
0x00000000b092309dLL,
0x000000000c9cba54LL,
0x0000000000d73f9fLL,
0x00000000000d73f9LL,
0x000000000000ca96LL
};
Wo im Festkommaformat. Dieses Mal habe ich der Einfachheithalberden gesamten Code auf dem Fast_Sine-Github veröffentlichtund Quadmath aus GrĂŒnden der KompatibilitĂ€t mit Clang und Arm entfernt. Und ich habe die Methode zur Berechnung des Fehlers ein wenig geĂ€ndert.
Der Vergleich der Standard-Sinusfunktion und der Festkommafunktion ist in den beiden folgenden Tabellen angegeben. Die erste Tabelle zeigt die Berechnungsgenauigkeit (fĂŒr x86_64 und ARM ist sie völlig gleich). Die zweite Tabelle ist ein Leistungsvergleich.
Funktion | Anzahl der Fehler | Maximaler ULP-Wert | Durchschnittliche Abweichung |
sin_e7 | 0,0822187% | 0,504787 | 7.10578e-20 |
sin_e7a | 0,0560688% | 0,503336 | 2,0985e-20 |
std :: sin | 0,234681% | 0,515376 | --- ---. |
WĂ€hrend des Tests wurde der "wahre" Sinuswert unter Verwendung der MPFR- Bibliothek berechnet... Der maximale ULP-Wert wurde als maximale Abweichung vom "wahren" Wert angesehen. Prozentsatz der Fehler - Die Anzahl der FĂ€lle, in denen der berechnete Wert der Sinusfunktion von uns oder von libm binary nicht mit dem auf den doppelten Sinus aufgerundeten Wert ĂŒbereinstimmte. Der Mittelwert der Abweichung zeigt die "Richtung" des Berechnungsfehlers: ĂberschĂ€tzung oder UnterschĂ€tzung des Wertes. Wie Sie der Tabelle entnehmen können, ĂŒberschĂ€tzt unsere Funktion die Sinuswerte. Dies kann behoben werden! Wer hat gesagt, dass die tsx-Werte genau den Koeffizienten der Taylor-Reihe entsprechen sollten? Eine ziemlich offensichtliche Idee bietet sich an, die Werte der Koeffizienten zu variieren, um die Genauigkeit der Approximation zu verbessern und die konstante Komponente des Fehlers zu entfernen. Es ist ziemlich schwierig, eine solche Variation korrekt vorzunehmen. Aber wir können es versuchen. Nehmen wir zum Beispiel4. Wert aus dem Array der tsx-Koeffizienten (tsx [3]) und Ă€ndern Sie die letzte Zahl a in f. Lassen Sie uns das Programm neu starten und die Genauigkeit (sin_e7a) sehen. Schau, es ist ein wenig, aber erhöht! Wir fĂŒgen diese Methode unserem Sparschwein hinzu.
Nun wollen wir sehen, was die Leistung ist. Zum Testen nahm ich das vorhandene i5 mobile und eine leicht ĂŒbertaktete vierte Himbeere (Raspberry PI 4 8 GB), GCC10, aus der Ubuntu 20.04 x64-Distribution fĂŒr beide Systeme.
Funktion | x86_64 Zeit, s | ARM-Zeit, s |
sin_e7 | 0,174371 | 0,469210 |
std :: sin | 0,154805 | 0,447807 |
Ich gebe nicht vor, bei diesen Messungen genauer zu sein. AbhĂ€ngig von der Prozessorlast sind Abweichungen von mehreren zehn Prozent möglich. Die Hauptschlussfolgerung kann so gezogen werden. Das Umschalten auf Ganzzahlarithmetik fĂŒhrt bei modernen Prozessoren 2 nicht zu einem Leistungsgewinn . Die unvorstellbare Anzahl von Transistoren in modernen Prozessoren ermöglicht die schnelle DurchfĂŒhrung komplexer Berechnungen. Ich denke jedoch, dass dieser Ansatz sowohl bei Prozessoren wie Intel Atom als auch bei schwachen Controllern zu einem erheblichen Leistungsgewinn fĂŒhren kann. Was denken Sie?
WĂ€hrend dieser Ansatz zu einem Genauigkeitsgewinn gefĂŒhrt hat, scheint dieser Genauigkeitsgewinn eher interessant als nĂŒtzlich zu sein. In Bezug auf die Leistung kann sich dieser Ansatz beispielsweise im IoT befinden. FĂŒr Hochleistungsrechner ist es jedoch kein Mainstream mehr. In der heutigen Welt bevorzugen SSE / AVX / CUDA die parallele Funktionsberechnung. Und in Gleitkomma-Arithmetik. Es gibt keine parallelen Analoga der MUL-Funktion. Die Funktion selbst ist eher eine Hommage an die Tradition.
Im nĂ€chsten Kapitel werde ich beschreiben, wie Sie AVX effektiv fĂŒr Berechnungen verwenden können. Lassen Sie uns noch einmal in den libm-Code gehen und versuchen, ihn zu verbessern.
1 Es gibt keine mir bekannten Register mit solchen Namen in Prozessoren. Die Namen wurden zum Beispiel gewÀhlt.
2Hierbei ist zu beachten, dass mein ARM mit der neuesten Version des Mathe-Coprozessors ausgestattet ist. Wenn der Prozessor Gleitkommaberechnungen emuliert, können die Ergebnisse erheblich abweichen.