erf-Funktion approximieren



  • Hey Leute,

    die sog. erf(x)-Funktion kann folgendermaßen angenähert werden:
    1(a_1t+a_2t2+a3t3)ex21-(a\_1t+a\_2t^2+a_3t^3)e^{-x^2} mit t=1/(1+px)t=1/(1+px) wobei die a's und p Konstanten(Konstantendefination am Codeanfang) sind.

    Aufgabe ist es die obige Approximation in eine Funktion zu packen. Und bei Eingabe einer reellen Zahl x, soll erf(x) ausgegeben werden. Das ganze soll in einer Schleife erfolgen, bis x=0 das Programm beendet. Das Ergebnis erf(0) soll jedoch noch an stdout geschrieben werden. Zusätzlich soll auch erf(x)=e(x)-erf(x)=e(-x) soll berücksichtigt werden.

    Fragen:
    1. Weitere Lösung zur Berechung von erf(x):

    if(x<0)
    {
      //erfx=(.....)-1;
    }
    else
    {
      //erfx=1-(.....);
    }
    

    Ich frage mich, ob die unten aufgeführte Lösung erf(x) zu berechnen besser ist als die oben erwähnte im gezeigten Codeausschnitt. Eigentlich ist ja obige Lösung kürzer, also müsste sie auch schneller sein oder? Und schneller ist in dem Fall ja besser. Oder was meint ihr?
    (Bei Null wird die if-else-Anweisung nicht berücksichtigt, aber das ist im Grunde ja egal, da erf(0)=0 ist und erfx mit Null initialisiert wird.)

    2. Gibt es sonst noch was bezüglich performance oder anderem auszusetzen?

    #include <stdio.h>
    #include <math.h>
    
    #define p 0.47047
    #define a_1 0.3480242
    #define a_2 -0.0958798
    #define a_3 0.7478556
    
    double ErrorFunction(double x);
    
    int main(void)
    {
      double x = 0; 
    
      do
      {
        printf("Enter any value x to show erf(x): ");
        scanf_s("%lf",&x);
    
        printf("\n erf(x)=%lf\n\n", ErrorFunction(x));
      } while (x != 0);
    
      return 0;
    }
    
    double ErrorFunction(double x)
    {
      double erfx = 0;
      double t = 0;
      double xBuffer = x;
    
      if (x < 0)
      {
        x = -x;
      }
    
      t=1 / (1 + p*x);
    
      erfx = 1 - (a_1*t + a_2*t*t + a_3*t*t*t)*exp(-(x*x));
    
      if(xBuffer < 0)
      {
        erfx = -erfx;
      }
    
      return erfx;
    }
    


  • Ich würde es erst einmal so lassen.

    Erst wenn deine Funktion zig tausend mal in einem Programmablauf aufgerufen wird, würde ich versuchen diese zu optimieren.



  • Welche Lösung schneller ist, ist nicht sicher. Jedenfalls ist Deine Implementierung länger. Und dazu noch durch die Makros zerissen.

    Ich würde zum "no-brainer" greifen und es einfach so runterschreiben wie es anfangs in Deinem Text steht:

    double erf_approx(double x) {
        const double a_1=0.3480242,
            a_2=-0.0958798,
            a_3=0.7478556,
            p=0.47047,
            t=1 / (1 + p*x);
    
        return 1 - (a_1*t + a_2*t*t + a_3*t*t*t)*exp(-(x*x));
    }
    
    double ErrorFunction(double x) {
        if (x<0.)
            return -erf_approx(-x);
        return erf_approx(x);
    }
    


  • Ah ok danke!

    1. Ist das reine Geschmackssache, ob ich die Formel jetzt extra in ein Unterprogramm mache? Also ich könnte ja gleich in "ErrorFunction()" return Formel schreiben.

    2. Was genau ist hier denn der Nachteil von konstanten Macros? Der definierte Name wird ja einfach nur einer Zahl zugeschrieben, also wenn ich PI schreibe, dann steht da eig. einfach 3.14 da, wenn ich halt auch #define PI 3.14 auch schreibe.

    Also für was verwende ich den type const und für was dann konstante macros? Also ich denke, wenn man Konstanten nur in einem Codeblock {} braucht, dann brauch ich ja kein Makro, denn das ist ja im Prinzip global.

    Ist das so gemeint?



  • Makros sind Scheiße.

    Und dazu noch durch die Makros zerissen.

    Das trifft den Punkt sehr gut.

    - sie haben immer global scope
    - haben ggü. richtigen Funktionen keine Kopiersemantik der Parameter, Überraschungen sind absehbar beim Gebrauch z.B.

    makro(i++); ggü. fkt(i++);
    

    - hierbei muss man also wissen, wann ein Makro vorliegt und wann nicht um entsprechend zu implementieren - dass schafft unnötige Abhängigkeiten+Fehlerquellen, denn wer möchte schon für jede aus einer benutzten Bibliothek verwendeten Funktion doch noch mal schauen müssen, ob sich nicht doch ein Makro dahinter verbirgt

    - bei Makros hat der Compiler keine Chance, irgendwelchen Unsinn zu erkennen (da der Präprozessor schon gelaufen ist; der Präprozessor wirft zwar auch Fehler aber arbeitet nie so detailliert wie der Compiler, welcher z.B. Typprüfungen der Parameter vornehmen kann, weil er Typen kennt die ein Präprozessor nicht kennt)
    - auch beim Debuggen sind richtige Funktionen sehr viel angenehmer als Makros
    - Makros zur Optimierung als inline-Ersatz einzusetzen ist ebenfalls naiver Unsinn, da man vermeintliche Geschw.vorteile durch tatsächliche Unwartbarkeit eintauscht

    Fazit: ohne Not Makros zu verwenden, grenzt an Masochismus, da man explizit den Compiler als Codereviewer ausschließt.

    Allenfalls sind reine #define für #ifdef/#if u.ä. passabel, da hier quasi explizit eine "Zerrissenheit" des Codes erwünscht ist

    Für Integer-Konstanten nimmt man statt #define immer enum, für Stringliterale nimmt man auch nie #define, da Stringliterale in C ggü. anderen Literalen immer Objekte sind, die somit einen Typ besitzen und das sollte man immer durch den Compiler prüfen lassen.

    Ich bin kein Freund von static, hier aber könnte man dem Compiler noch einen Wink geben für häufiges Aufrufen der Funktion eine Optimierung zu spendieren, wenn er es nicht sowieso schon macht:

    double erf_approx(double x) {
        static const double a_1=0.3480242,
            a_2=-0.0958798,
            a_3=0.7478556,
            p=0.47047,
            t=1 / (1 + p*x);
    
        return 1 - (a_1*t + a_2*t*t + a_3*t*t*t)*exp(-(x*x));
    }
    

    Wie immer gilt aber bei solch allgemeinen Betrachtungen:
    Profis, die ihren eigenen Code seit 20 Jahren kennen und warten, werden sich kaum an solche allgemeinen Regeln halten - die setzen also durchaus Makros/goto/... ein - der Haken liegt aber dann vor, wenn Neulinge den Code warten müssen die ihn eben nicht schon seit 20 Jahren kennen



  • C_Boy schrieb:

    1. Ist das reine Geschmackssache, ob ich die Formel jetzt extra in ein Unterprogramm mache? Also ich könnte ja gleich in "ErrorFunction()" return Formel schreiben.

    IMHO wird das vom Sourcecode her nie so klar, wie mit der zweiten Funktion.
    Deine Blöcke werden länger, Du wiederholst Code, Du musst irgendwo eine weitere Fallunterscheidung machen etc. pp.

    Ich zeig Dir, dass Du keine Angst wg. der Performanz haben musst und - en passant - noch ein kleines Ärgernis ausbügelst.
    Das kleine Ärgernis:
    erf_approx ist ein Helferlein, dass Du besser versteckst. Es wird nämlich bald jemand kommen und erf_approx mit negativem x aufrufen.

    Dabei hilft Dir eine Kombination von Sourcecodeorganisation und Attributen der Funktion erf_approx :

    // error_function.h:
    #ifndef ERROR_FUNCTION_H
    #define ERROR_FUNCTION_H
    
    double ErrorFunction(double x);
    
    #endif
    
    // error_function.c:
    #include <math.h>
    #include "error_function.h"
    
    static inline double erf_approx(double x) {
        static const double a_1=0.3480242,
            a_2=-0.0958798,
            a_3=0.7478556,
            p=0.47047;
        const double t=1 / (1 + p*x);
    
        return 1 - (a_1*t + a_2*t*t + a_3*t*t*t)*exp(-(x*x));
    }
    
    double ErrorFunction(double x) {
        if (x<0.)
            return -erf_approx(-x);
        return erf_approx(x);
    }
    

    Der Witz ist jetzt Z.5 in error_function.c : erf_approx ist jetzt static, d.h. nur in error_function.c überhaupt aufrufbar. Und als kleiner hint an den geneigten Leser und den Compiler bitte ich auch noch darum diese Funktion zu inlinen, damit stehen die Chancen hoch, dass niemals überhaupt Code für arf_approx generiert wird.
    D.h. damit hast Du so ziemlich das, was man möchte: keinen Overhead und klaren Source.

    Hier kannst Du Dir das mal ansehen: https://godbolt.org/g/Vn9mY2
    Ohne Optimierungen (-O99, rechts oben) wird nie geinlined (inline ist nur eine Bitte an den Compiler) und mit Optimierungen eigentlich immer. Dazu mal das inline aus der Definition nehmen und mit -O99 compilen.



  • Danke für die Antworten. Es ist jetzt klarer.

    Ich habe eine andere Lösung gefunden, womit ich nicht zweimal dieselbe Code Line habe, aber bin mir nicht sicher, ob das doch nicht irgendwelche Fehler verbergen kann:

    double ErrorFunction(double x)
    {  
    
      static const double a_1=0.3480242,
            a_2=-0.0958798,
            a_3=0.7478556,
            p=0.47047;
      double t=0;
    
      if (x < 0)
      {
        return -ErrorFunction(-x);
      }
    
      t = 1 / (1 + p*x);
    
      return 1 - (t*(a_1 + t*(a_2 + t*a_3)))*exp(-(x*x));
    }
    

    Ist es okay, wenn man im Unterprogramm selber nochmal das Unterprogramm selbst aufruft?

    Die t's habe ich auch rausgehoben, um das ganze nur auf 3 Multiplikationen zu beschränken.



  • C_Boy schrieb:

    Ist es okay, wenn man im Unterprogramm selber nochmal das Unterprogramm selbst aufruft?

    Das ist eine Rekursion.
    Viele finden das sogar sehr elegant!

    Mir gefällt Deine Lösung auch sehr gut. Jetzt lass aber noch die Makros verschwinden.

    Das mit dem t rausziehen ist übrigens super. Schau mal, wie der vom Compiler generierte Code zusammenschrumpft! Offensichtlich hilft das ungemein.

    Viel Erfolg!



  • @Wutz
    Folgendes extrem hässliches Makro habe ich neulich in der SDK von Nordic gefunden:

    #define APP_UART_FIFO_INIT(P_COMM_PARAMS, RX_BUF_SIZE, TX_BUF_SIZE, EVT_HANDLER, IRQ_PRIO, ERR_CODE) \
        do                                                                                             \
        {                                                                                              \
            uint16_t           APP_UART_UID = 0;                                                       \
            app_uart_buffers_t buffers;                                                                \
            static uint8_t     rx_buf[RX_BUF_SIZE];                                                    \
            static uint8_t     tx_buf[TX_BUF_SIZE];                                                    \
                                                                                                       \
            buffers.rx_buf      = rx_buf;                                                              \
            buffers.rx_buf_size = sizeof (rx_buf);                                                      \
            buffers.tx_buf      = tx_buf;                                                              \
            buffers.tx_buf_size = sizeof (tx_buf);                                                      \
            ERR_CODE = app_uart_init(P_COMM_PARAMS, &buffers, EVT_HANDLER, IRQ_PRIO, &APP_UART_UID);   \
    } while (0)
    

    https://github.com/Seeed-Studio/mbed_ble/blob/master/nRF51822/nordic/nrf-sdk/app_common/app_uart.h

    PS:
    Ich wäre froh wenn viele Leute deinen Rat beherzigen würden: Makros sind Scheiße.



  • Bitte ein Bit schrieb:

    PS:
    Ich wäre froh wenn viele Leute deinen Rat beherzigen würden: Makros sind Scheiße.

    Ach, das kann man so pauschal nicht sagen.

    Für einen Datumsschreiber und -Interpreter definiere ich mir ein paar Makros, die sich darum kümmern, einen bestimmten String in einen Buffer zu schreiben/ihn von einem Buffer zu lesen.

    Die Alternative mit Funktionen?
    Benötigt zunächst mal einen Haufen Parameter, die deklariert werden müssen.
    Die Fehlerbehandlung wird sehr schnell kompliziert, da nicht genug Speicher für die gegenwärtige Aktion nicht in einem Fehler, aber je nach Feld das Schreiben/Lesen von 0 zu einem Fehler führen soll (Tage und Monate sind nicht 0, aber Stunden, Minuten, Sekunden).
    Viel Code wird zwischen den verschiedenen Makros geteilt. In der derzeitigen Fassung kann ich diese Teile noch mal in einem separaten Makro abstrahieren. Mit einer Funktion habe ich noch mal zusätzlichen Overheadcode für praktisch keinen Nutzen. Gleiches gilt auch für Parametersetzung innerhalb von Makros - wenn ich einen zweistelligen Wert schreiben will, kann die Quelle eine Stunde, eine Minute oder eine Sekunde sein, sprich, nur das Feld ändert sich. Mit einem Makro definiere ich die Quellsetzung in zwei Zeilen; selbst mit geinlineten Funktionen komme ich auf mindestens +5 Zeilen pro Ersetzung.

    Wenn ich auf den modularen, semi-automatischen Ansatz verzichte, kann ich Schreiber/Leser nicht mehr in weniger als 15 Zeilen definieren.

    Irgendwer voreiliges schrieb:

    Nimm doch strftime

    Die Funktion hat kaputte Fehlererkennung, ist lokalenabhängig und damit auf globale RW-Ressourcen angewiesen (sprich, entweder sind diese gelockt, was scheiße ist, oder sie sind nicht gelockt, was noch sehr viel mehr scheiße ist), und benötigt ein zusätzliches Byte für die NUL-terminierung. Ich kann mit Abstand keine Funktion nennen, die ich für kaputter erachte. Und das schließt mktime mit ein.

    Ja, Makros werden oft für Scheißpraktiken missbraucht. Makros sind oft scheiße. Aber nicht immer.



  • Ohje, Makros und static, das kann nicht gut gehen.

    Ist absoluter Schwachsinn, denn ruft man das Makro z.B. in einer Schleife auf,
    wird beim jeweils folgenden Aufruf z.B. für eine 2.UART die Initialisierung mit den Hinterlassenschaften der 1.UART durchgeführt, was sicher nicht gewünscht ist:

    int x;
    P_COMM_PARAMS uart[3];
    for(int i=0;i<3;i++)
    {
    APP_UART_FIFO_INIT(uart[i], 3, 3, NULL, 0, x); <- hier knallts dann bei i>0
    }
    

    Man muss also wissen, dass man dieses Makro nicht in Schleifen verwenden darf - purer Nonsens.
    Ebenso muss man "wissen", dass bei Verwendung des Makros

    APP_UART_FIFO_INIT(p1,p2,p3,p4,p5,p6);
    

    p2 und p3 Compilezeitkonstanten sein müssen (wegen static); ignoriert man die Compilerwarnungen - was insbesondere Ahnungslose gern tun um das Programm "erstmal" lauffähig zu machen - gibts dann Überraschungen zur Laufzeit - bestenfalls Abstürze.
    Weiterhin funktioniert die o.g. Variante der for-Schleife bei einem Durchlauf, bei mehr aber nicht mehr -> Auswirkungen von static-Verwendung.

    Ebenso jammern diese Hardwarefrickler immer rum, dass sie zu wenig Speicher hätten - und hier verwenden sie kostbaren static Speicher aus Bequemlichkeit und Unwissen - Deppen halt, denn bei jeder Referenz dieses Makros werden RX_BUF_SIZE+TX_BUF_SIZE Bytes static-Speicher unwiederbringlich verbraten - wobei zu befürchten ist, dass der ganze Code von solchem Unsinn durchsetzt ist und nicht immer ist solch Unsinn sofort zu erkennen wie hier.

    Professionell würde man sowas C99 konform mit compound literals machen, z.B.

    int x  = app_uart_init(P_COMM_PARAMS1, &(app_uart_buffers_t){0}, EVT_HANDLER, IRQ_PRIO, &(int8_t){0});
    int x1 = app_uart_init(P_COMM_PARAMS2, &(app_uart_buffers_t){0}, EVT_HANDLER, IRQ_PRIO, &(int8_t){0});
    

    So hat jede UART ihren eigenen - immer 0 initialisierten - exklusiven Speicher,
    ohne sich den static Speicher zuzumüllen, und um das Wegräumen dieses Speichers braucht man sich auch nicht zu kümmern, da free() hier ja entfällt.

    Fehlt bloß noch zu wissen, welche Hardware ich zukünftig meiden muss, um nicht in Abhängigkeit von solchem Schrottcode zu kommen.



  • Ich hätte da noch eine Frage bitte: Ich nehme an, dass es immer optimal ist bereits in eine Bibliothek implementierte Funktionen wie z.B. scanf() auf Rückgabewerte zu prüfen.
    Das Programm soll sich schließen, wenn ich keine Zahlen eingebe. Ich habe das mal ein zwei verschiedenen Versionen gemacht.
    (Ich benutze den VS 2017 Compiler, darum scanf_s)

    In der 1. Version meines main-Programms habe ich exit() benutzt. Aber man sollte goto, labels, exit, break und diese Sachen ja möglichst meiden, da diese die Struktur im Prinzip kaputt machen. Ist das so richtig gedacht?

    Darum habe ich dann eine 2. Version angefertigt, um das Program auf normale Art und Weise beenden zu lassen. Also das Programm läuft bis zum ender des main-Blocks.

    Ich vermute mal, dass die 2. Version die bessere Lösung ist. Aber gibts noch Verbesserungsmöglichkeiten bzw. etwas auszusetzen?

    main_v1:

    int main(void)
    {
      double x = 0;
    
      do
      {
        printf("Enter any value x to show erf(x): ");
    
        if (scanf_s("%lf", &x) != 1)
        {
          printf("error: invalid input\n");
          exit(EXIT_FAILURE);
        }
        else
        {
          printf("\n erf(x)=%f\n\n", ErrorFunction(x));
        }
    
      } while (x != 0);
    
      return 0;
    }
    

    main_v2:

    int main(void)
    {
      double x = 0;
      int i = 0; 
    
      do
      {
        printf("Enter any value x to show erf(x): ");
        i = scanf_s("%lf", &x);
    
        if (i == 1)
        {
          printf("\n erf(x)=%f\n\n", ErrorFunction(x));
        }
    
      } while (x != 0 && i == 1);
    
      if (i != 1)
      {
        printf("error: invalid input\n");
      }
    
      return 0;
    }
    


  • Aus der main-Funktion heraus kannst du auch einfach

    return EXIT_FAILURE;
    

    schreiben.



  • Ich finde deine v1 schöner. Nach dem potentiell fehlerauslösenden Call gleich der Check und bei Fehler sofort raus aus der Funktion (also z.B. break, return, exit).

    Was aber unschön ist, ist dein "else"-Zweig. Das else ist hier überflüssig. Wenn dein scanf nicht erfolgreich ist, springst du ja schon raus. Daher lass das else weg. Dadurch verringert sich auch der Einzug für das printf der ErrorFunction.



  • Ahh, stimmt. Danke euch!

    v3:

    int main(void)
    {
      double x = 0;
    
      do
      {
        printf("Enter any value x to show erf(x): ");
    
        if (scanf_s("%lf", &x) != 1)
        {
          printf("error: invalid input\n");
          return EXIT_FAILURE;
        }
    
        printf("\n erf(x)=%f\n\n", ErrorFunction(x));
      } while (x != 0);
    
      return 0;
    }
    

    D.h. v3 ist am "besten"? Ich dachte eben, dass man exit, goto, break etc. eher meiden sollte, da sie die Struktur des Programms kaputt machen oder halt "zerreißen" so wie die Macros.


  • Mod

    C_Boy schrieb:

    Aber ich dachte halt, dass man exit, goto, break etc. eher meiden sollte, da sie die Struktur des Programms kaputt machen oder halt "zerreißen" so wie die Macros.

    Was sagt ihr dazu?

    Das ist durchaus richtig.

    So ein Abbruch mitten im Programm ist oft auch eine maßlose Überschreitung von Zuständigkeiten, der verhindert, dass Code wiederverwendet werden kann. Beispielsweise ist eine Eingabeprüfung, die im Fehlerfall das ganze Programm unterbricht ziemlich unflexibel. Da bei dir derzeit sowieso keine Trennung von Aufgaben stattfindet, sondern das gesamte Programm aus einem einzigen Stück besteht, macht es aber bei dir derzeit noch keinen Unterschied.


Anmelden zum Antworten