Gleitkomma-Linearinterpolation

Lesezeit: 10 Minuten

Benutzeravatar von Thomas O
Thomas o

Um eine lineare Interpolation zwischen zwei Variablen durchzuführen a und b einen Bruchteil gegeben fverwende ich derzeit diesen Code:

float lerp(float a, float b, float f) 
{
    return (a * (1.0 - f)) + (b * f);
}

Ich denke, es gibt wahrscheinlich einen effizienteren Weg, dies zu tun. Ich verwende einen Mikrocontroller ohne FPU, daher werden Gleitkommaoperationen in Software ausgeführt. Sie sind ziemlich schnell, aber es sind immer noch etwa 100 Zyklen zum Addieren oder Multiplizieren.

Irgendwelche Vorschläge?

Hinweis: Aus Gründen der Übersichtlichkeit in der Gleichung im obigen Code können wir die Angabe weglassen 1.0 als explizites Fließkommaliteral.

Benutzeravatar von aioobe
aiobe

Wie Jason C in den Kommentaren betont, ist die von Ihnen gepostete Version aufgrund ihrer überlegenen Präzision in der Nähe der Randfälle höchstwahrscheinlich die beste Wahl:

float lerp(float a, float b, float f)
{
    return a * (1.0 - f) + (b * f);
}

Wenn wir die Genauigkeit für eine Weile außer Acht lassen, können wir den Ausdruck wie folgt vereinfachen:

a(1 − f) × (ba)
= aaf + bf

= a + f(ba)

Das heißt, wir könnten es so schreiben:

float lerp(float a, float b, float f)
{
    return a + f * (b - a);
}

In dieser Version haben wir eine Multiplikation entfernt, aber etwas Genauigkeit verloren.

  • Der ursprüngliche Algorithmus ist auch in Bezug auf die Leistung kein großer Verlust: Die FP-Multiplikation ist viel schneller als die FP-Addition, und wenn f liegt garantiert zwischen 0 und 1, gewisse Optimierungen dazu (1-f) Sind möglich.

    – Sneftel

    17. Mai 2014 um 23:29 Uhr


  • @Sneftel: Können Sie die Optimierungen für 1 - f? Ich bin zufällig in dieser Situation und bin neugierig 😀

    – Levi Morrison

    11. September 2014 um 1:54 Uhr

  • @LeviMorrison Grundsätzlich garantiert das, dass der Exponent für f ist nichtpositiv (und garantiert natürlich feste Werte für Mantisse und Exponent von 1), die meisten Zweige in der Subtraktionsroutine ausschneiden.

    – Sneftel

    11. September 2014 um 8:18 Uhr

  • @coredump Tut mir leid, dass ich deinen Kommentar vor 2 Jahren nicht bemerkt habe (heh …). Die OP’s wären noch genauer, insbesondere wenn f * (b - a) unterscheidet sich deutlich von der Größenordnung a bei diesem Algorithmus fällt dann die Addition auseinander. Es ist die Addition/Subtraktion, bei der Sie auf Probleme stoßen. Das heißt, selbst die OPs können scheitern, wenn f ist zu groß im Verhältnis zu 1.0fwie 1.0f - f gleich werden könnte -f für sehr groß f. Wenn Sie also mit riesigen Werten arbeiten, z f Sie müssen ein wenig über die Mathematik nachdenken. Das Problem ist, dass Sie auf Dinge wie stoßen 1.0 + 1.0e800 == 1.0e800.

    – Jason C

    20. März 2017 um 0:48 Uhr


  • Stellen Sie sich Fließkommazahlen einfach als Festkomma-Mantissen und einen Exponenten vor (es ist komplizierter, aber sie so zu sehen schon reicht aus zu erkennen viele Problemzonen). Wenn Sie also die Genauigkeit der Mantisse überschreiten, verlieren Sie Informationen. Konzeptionell ähnlich der Tatsache, dass wir zum Beispiel 1.230.000 nicht dezimal mit nur zwei signifikanten Ziffern darstellen können (1,2 * 10 ^ 6 ist das nächste, was wir bekommen können), also wenn Sie 1.200.000 + 30.000 tun, aber nur zwei signifikante Ziffern haben Ihrer Verfügung, verlieren Sie diese 30.000.

    – Jason C

    20. März 2017 um 0:55 Uhr


Benutzeravatar von Jason C
Jason C

Unter der Annahme, dass Gleitkommamathematik verfügbar ist, ist der Algorithmus des OP gut und der Alternative immer überlegen a + f * (b - a) aufgrund von Präzisionsverlust, wenn a und b unterscheiden sich deutlich in der Größenordnung.

Zum Beispiel:

// OP's algorithm
float lint1 (float a, float b, float f) {
    return (a * (1.0f - f)) + (b * f);
}

// Algebraically simplified algorithm
float lint2 (float a, float b, float f) {
    return a + f * (b - a);
}

In diesem Beispiel gehen wir von 32-Bit-Gleitkommazahlen aus lint1(1.0e20, 1.0, 1.0) wird korrekt 1.0 zurückgeben, wohingegen lint2 gibt fälschlicherweise 0.0 zurück.

Der Großteil des Genauigkeitsverlusts liegt in den Additions- und Subtraktionsoperatoren, wenn sich die Operanden in der Größe erheblich unterscheiden. Im obigen Fall sind die Übeltäter die Subtraktion in b - aund der Zusatz in a + f * (b - a). Der Algorithmus des OP leidet darunter nicht, da die Komponenten vor der Addition vollständig multipliziert werden.


Für die a=1e20, b=1 Fall, hier ist ein Beispiel für unterschiedliche Ergebnisse. Testprogramm:

#include <stdio.h>
#include <math.h>

float lint1 (float a, float b, float f) {
    return (a * (1.0f - f)) + (b * f);
}

float lint2 (float a, float b, float f) {
    return a + f * (b - a);
}

int main () {
    const float a = 1.0e20;
    const float b = 1.0;
    int n;
    for (n = 0; n <= 1024; ++ n) {
        float f = (float)n / 1024.0f;
        float p1 = lint1(a, b, f);
        float p2 = lint2(a, b, f);
        if (p1 != p2) {
            printf("%i %.6f %f %f %.6e\n", n, f, p1, p2, p2 - p1);
        }
    }
    return 0;
}

Ausgabe, leicht angepasst für die Formatierung:

    f            lint1               lint2             lint2-lint1
0.828125  17187500894208393216  17187499794696765440  -1.099512e+12
0.890625  10937500768952909824  10937499669441282048  -1.099512e+12
0.914062   8593750447104196608   8593749897348382720  -5.497558e+11
0.945312   5468750384476454912   5468749834720641024  -5.497558e+11
0.957031   4296875223552098304   4296874948674191360  -2.748779e+11
0.972656   2734375192238227456   2734374917360320512  -2.748779e+11
0.978516   2148437611776049152   2148437474337095680  -1.374390e+11
0.986328   1367187596119113728   1367187458680160256  -1.374390e+11
0.989258   1074218805888024576   1074218737168547840  -6.871948e+10
0.993164    683593798059556864    683593729340080128  -6.871948e+10
1.000000                     1                     0  -1.000000e+00

  • Interessanterweise ist die Version von OP nicht immer überlegen. Ich dachte, es wurde dann von diesem Beispiel gebissen: lerp(0.45, 0.45, 0.81965185546875). Es sollte offensichtlich 0,45 geben, aber zumindest für doppelte Genauigkeit bekomme ich 0,45000000000000007, während die a + (ba) * f-Version eindeutig a ergibt, wenn a==b. Ich würde gerne einen Algorithmus sehen, der die Eigenschaft hat, dass lerp(a, b, f) kehrt zurück a wenn f==0, b wenn f==1und bleibt im Bereich [a,b] zum f in [0,1].

    – Ben

    22. Dezember 2016 um 15:22 Uhr

  • Zuerst brauchen Sie den Fall if a == b -> return a. Genau 0,45 kann jedoch nicht mit doppelter oder Gleitkommagenauigkeit dargestellt werden, da es sich nicht um eine exakte Potenz von 2 handelt. In Ihrem Beispiel alle Parameter a, b, f werden innerhalb des Funktionsaufrufs als Double gespeichert – Rückgabe a würde niemals genau 0,45 zurückgeben. (Bei explizit typisierten Sprachen wie C natürlich)

    – Benjamin R

    20. März 2017 um 0:29 Uhr


  • Das sieht nach der besseren Wahl aus. Interessanterweise scheint jedoch die Standardbibliothek lerp mit dem zu gehen algebraisch vereinfachte Version. Gedanken?

    – Lorah Attkins

    3. September 2021 um 9:55 Uhr

  • @ Don Gut; die Tatsache ist relevant, weil sie der Kern von Bens Beobachtung ist; Was übersehen wurde, ist, dass seine Verbindung zur lerp-Implementierung ein Ablenkungsmanöver ist: Ja lerp(a, a, anything) sollte zurückkehren aaber 0,45 kann nicht dargestellt werden und ist daher außerhalb der Domäne dieser Funktion, und deshalb macht es keinen Sinn, darüber zu sprechen. Beachten Sie auch, dass beide Versionen von lerp nicht genau 0,45 ergeben würden. Eben return 0.45 würde nicht 0,45 zurückgeben. Programmierer, die solche Sprachen verwenden, erwähnen dies im Allgemeinen jedoch nicht, da es normalerweise implizit und uninteressant ist.

    – Jason C

    19. Oktober 2021 um 0:33 Uhr

  • @LorahAttkins während der C++-Standard spezifiziert std::lerp als Berechnung von $a+t(ba)$, das heißt nur wird als mathematische Definition dessen verwendet, was die Funktion berechnet. Der Standard legt zusätzlich weitere Einschränkungen bei der Implementierung von fest std::lerp: es muss monoton sein, es muss präzise sein für $t\in\{0,1\}$ und $a = b$. Das bedeutet weder lint1 Noch lint2 sind gültige Implementierungen von std::lerp. Als solches wird niemand verwenden std::lerp weil es zu verzweigt und langsam ist.

    – CAD97

    19. November 2021 um 22:06 Uhr


Benutzeravatar von Jim Credland
Jim Credland

Wenn Sie sich auf einem Mikrocontroller ohne FPU befinden, wird Gleitkomma sehr teuer. Könnte für eine Gleitkommaoperation leicht zwanzigmal langsamer sein. Die schnellste Lösung besteht darin, die gesamte Berechnung mit Ganzzahlen durchzuführen.

Die Anzahl der Stellen nach dem festen Binärkomma (http://blog.credland.net/2013/09/binary-fixed-point-explanation.html?q=fixed+binary+point) ist: XY_TABLE_FRAC_BITS.

Hier ist eine Funktion, die ich verwende:

inline uint16_t unsignedInterpolate(uint16_t a, uint16_t b, uint16_t position) {
    uint32_t r1;
    uint16_t r2;

    /* 
     * Only one multiply, and one divide/shift right.  Shame about having to
     * cast to long int and back again.
     */

    r1 = (uint32_t) position * (b-a);
    r2 = (r1 >> XY_TABLE_FRAC_BITS) + a;
    return r2;    
}

Mit der Funktion inline sollte es ca. 10-20 Zyklen.

Wenn Sie einen 32-Bit-Mikrocontroller haben, können Sie größere Ganzzahlen verwenden und größere Zahlen oder mehr Genauigkeit erhalten, ohne die Leistung zu beeinträchtigen. Diese Funktion wurde auf einem 16-Bit-System verwendet.

  • Ich habe die Website gelesen, bin aber immer noch etwas verwirrt, welche Position sein sollte. Ist das ein Wert von 0 bis 0xFFFF? oder 0 bis 0xFFFE? Was ist auch XY_TABLE_FRAC_BITS? 8?

    – jjxtra

    5. April 2017 um 22:15 Uhr

  • @jjxtra: XY_TABLE_FRAC_BITS ist nur die (schlecht) benannte ganzzahlige Konstante, deren Wert angibt, wo sich der angenommene Binärpunkt in den verwendeten Festkomma-Ganzzahlwerten befindet (da er nicht in ihnen “herumschwebt”, wie dies bei Gleitkommazahlen der Fall ist).

    – Martinau

    17. Oktober 2018 um 18:45 Uhr

Wenn Sie für einen Mikrocontroller ohne Gleitkommaoperationen codieren, ist es besser, überhaupt keine Gleitkommazahlen zu verwenden und zu verwenden Festpunktarithmetik stattdessen.

Benutzeravatar von tambre
Tambre

Seit C++20 können Sie verwenden std::lerp()was wahrscheinlich die bestmögliche Implementierung für Ihr Ziel ist.

  • std::lerp sollte meiner Meinung nach genau nirgendwo verwendet werden. Sehr selten benötigen Sie tatsächlich beide Interpolationen und Extrapolation, plus eine Menge Verzweigungsverhalten, auf die numerisch instabile interne Implementierung. Ich habe so viele Meinungsverschiedenheiten darüber, wie std::lerp implementiert wurde, ist es schwer zu empfehlen.

    – jeremyong

    22. September 2020 um 0:35 Uhr


  • @jeremyong kannst du ein Beispiel für einen Fall geben, in dem std::lerp tut schlecht? Sein Kontrakt sieht in mehreren wichtigen Punkten sicherlich gut aus: Er ist monoton, lerp(a,b,0)==a, lerp(a,b,1)==b (und diese beiden Tatsachen implizieren, dass er im Bereich bleibt [a,b] für t ein [0,1]), lerp(a,a,t)==a. Die üblichen Beschwerden scheinen also abgedeckt zu sein.

    – Don Hatch

    19. Oktober 2021 um 0:34 Uhr

Es sei darauf hingewiesen, dass die standardmäßigen linearen Interpolationsformeln f1

Diese Funktion hat für mich auf Prozessoren funktioniert, die IEEE754-Gleitkomma unterstützen, wenn ich brauche, dass sich die Ergebnisse gut verhalten und die Endpunkte genau treffen (ich verwende sie mit doppelter Genauigkeit, aber Float sollte auch funktionieren):

double lerp(double a, double b, double t) 
{
    if (t <= 0.5)
        return a+(b-a)*t;
    else
        return b-(b-a)*(1.0-t);
}

  • std::lerp sollte meiner Meinung nach genau nirgendwo verwendet werden. Sehr selten benötigen Sie tatsächlich beide Interpolationen und Extrapolation, plus eine Menge Verzweigungsverhalten, auf die numerisch instabile interne Implementierung. Ich habe so viele Meinungsverschiedenheiten darüber, wie std::lerp implementiert wurde, ist es schwer zu empfehlen.

    – jeremyong

    22. September 2020 um 0:35 Uhr


  • @jeremyong kannst du ein Beispiel für einen Fall geben, in dem std::lerp tut schlecht? Sein Kontrakt sieht in mehreren wichtigen Punkten sicherlich gut aus: Er ist monoton, lerp(a,b,0)==a, lerp(a,b,1)==b (und diese beiden Tatsachen implizieren, dass er im Bereich bleibt [a,b] für t ein [0,1]), lerp(a,a,t)==a. Die üblichen Beschwerden scheinen also abgedeckt zu sein.

    – Don Hatch

    19. Oktober 2021 um 0:34 Uhr

Benutzeravatar von waterproof
wasserdicht

Wenn Sie möchten, dass das Endergebnis eine Ganzzahl ist, ist es möglicherweise schneller, auch Ganzzahlen für die Eingabe zu verwenden.

int lerp_int(int a, int b, float f)
{
    //float diff = (float)(b-a);
    //float frac = f*diff;
    //return a + (int)frac;
    return a + (int)(f * (float)(b-a));
}

Dadurch werden zwei Casts und ein Float multipliziert. Wenn ein Cast auf Ihrer Plattform schneller ist als ein Float-Addieren/Subtrahieren und wenn eine Integer-Antwort für Sie nützlich ist, könnte dies eine vernünftige Alternative sein.

  • Zum f * (b - a)die Typenförderung gewährt dies (b - a) befördert wird float Weil f ist vom Typ float. Also, die explizite Besetzung zu (float) in (float)(b - a) ist bestenfalls illustrativ, aber eigentlich nicht nötig, oder?

    – Scheffs Katze

    1. April 2020 um 16:12 Uhr

  • @Scheff – ja, du hast Recht, die Float-Besetzung wird nur ausgeschrieben, um auf etwas aufmerksam zu machen, das der Compiler sowieso einfügen wird.

    – mtrw

    1. April 2020 um 19:04 Uhr

1408320cookie-checkGleitkomma-Linearinterpolation

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

Privacy policy