Tempovergleich mit C



  • inspiriert durch die neuliche Frage nach schnellen Sprachen außer C/C++ und
    Fortran habe ich ein paar Tempo-Vergleiche Forth vs C angestellt.

    Hier mal was über Floating Point:

    Die Programme sind bewußt auf Vergleichbarkeit hin formuliert, also keine
    besonders trickreichen Optimierungen zugunsten einer der beiden Kandidaten
    usw.

    Aufgabe:

    Berechne Summe [1...LIMIT-1] sin^2(sqrt(f^3+f)) + cos^2(sqrt(f^3+f))
    

    (Ergebnis: LIMIT-1)

    wobei LIMIT := 10000001

    1) C compiler
    2) C compiler -O3
    3) C compiler -O2
    4) Forth
    

    Laufzeiten bei mir:

    1) 3.5 sec  <--
    2) 3.2 sec
    3) 3.2 sec
    4) 1.9 sec  <--
    

    der Fairness halber - Optimierung des C-Programms:

    weil im Forth-Programm bereits berechnete Werte auf dem Stack wiederverwendet
    werden, sollte ich das dem C-Programm auch gönnen:

    Ersetze flop(..) durch:

    double flop(double f){
      double g = sqrt(f*f*f+f);
      double si = sin(g);
      double co = cos(g);
      return(si*si + co*co);
    }
    

    Damit erhalte ich:

    5) C compiler:           1.9 sec  <--
    6) C compiler -O3:       1.1 sec
    4) Forth:                1.9 sec  <--
    

    Jetzt - mit optimierter Funktion flop(..) - sind beide auf Augenhöhe.

    Das Ergebnis wundert mich etwas.

    Schließlich erzeugen Forth-Compiler
    typischerweise keinen Maschinencode, sondern "threaded code", bei dem sich ein
    Interpreter von Sprungadresse zu Sprungadresse durch ein Dictionary hangelt,
    wogegen der C-Compiler echten Maschinencode produziert.

    So eine Aufgabe ist doch eigentlich ein "Heimspiel" für den C-Compiler.

    10000001 constant LIMIT
    : flop 
        fdup fdup fdup f* f* f+ fsqrt fdup fsin fdup f* fswap fcos fdup f* f+ ;
    : main                                
        0e0
        LIMIT 1 do
            i s>f
            flop f+
        loop
        f. cr ;
    main bye
    
    #include<stdio.h>
    #include<math.h>
    const long int LIMIT = 10000001;
    double flop(double f){
      return(sin(sqrt((f*f*f+f)))*sin(sqrt((f*f*f+f))) \
    +cos(sqrt((f*f*f+f)))*cos(sqrt((f*f*f+f))));
    }
    int main(int argc, char* argv[]){
      long int i;
      double sum;
      for(i = 1; i < LIMIT; i++)
        sum += flop((double)i);
      printf("%g\n", sum);
    }
    


  • Welches Forth-System denn? Auch solltest du aufpassen, dass du hier nicht die Implementation der Winkelfunktionen miteinander vergleichst.

    Schließlich erzeugen Forth-Compiler typischerweise keinen Maschinencode, sondern "threaded code", bei dem sich ein Interpreter von Sprungadresse zu Sprungadresse durch ein Dictionary hangelt, wogegen der C-Compiler echten Maschinencode produziert.

    Nicht zwingend, Forth-Systeme koennen auch on-the-fly Maschinencode erzeugen. Er ist nicht besonders optimiert, wie dein Beispiel zeigt. Ich habe schon mal kurz drueber nachgedacht, einen kleinen Artikel hier dazu zu verfassen - runtime machine code generation

    Die Programme sind bewußt auf Vergleichbarkeit hin formuliert

    Wie gesagt, sind sie nicht. Besser ist vielleicht sin und cos in beiden Sprachen als Reihe selbst zu implementieren und dann zu testen. Und wenn du schon dabei bist, kannst du ja auch gleich mal Factor mit einbeziehen.



  • hab' jetzt:

    1. der Einfachheit halber die Summe über sin2(2/i)+cos2(1/i) berechnet,
    also kein sqrt(i^3+i) mehr

    2. dabei sin und cos durch Reihen mit + - / * approximiert
    - und zwar so weit, daß keine Rundungsfehler auftreten
    und beide Programme das korrekte Ergebnis ausgeben,

    3. im C-Programm auf long double erhöht (double reicht nicht)

    Dann steht es bei mir 0.86 sec (C) : 2.4 sec (Forth)

    - also Welten auseinander ist das auch nicht.

    Man darf ja nicht vergessen, daß Forth interaktiv
    und weitgehend metazirkulär ist und kein typischer "number cruncher".

    10000002 constant LIMIT
    
    : finj fover fswap s>f fswap ;
    
    : freci 1e0 fswap f/ ;
    
    : finner 
        frot fdup f* f*                     
        fswap freci                         
        fswap f- ;
    
    : fsin_ ( f: f -- f' )
    
        \  where f'= f*(1 - f^2*(1/6 - f^2*(1/120 - f^2*(1/5040 - f^2/362880))))
    
        fdup 5040e0 362880e0 freci
        finner            
        120 finj          
        finner            
        6 finj            
        finner            
        1 finj            
        finner            
        f* ;
    
    ... usw ...
    


  • die Reihenapproximation von sin und cos gerade nochmal überarbeitet.

    Jetzt sehen die Laufzeiten so aus:

    C compiler -O3:   0.7 sec
    C compiler:       0.9 sec
    Forth:            1.3 sec
    
    : fsin+ ( f: f -- f' )                  \  f' = series approximation of sin
        fdup fsum f!
        fdup fdup f* fswap                  
    
        fover f* fdup                       
        -6e0 f/                             
        fsum f@ f+                          
        fsum f!                             
    
        fover f* fdup
        120e0 f/
        fsum f@ f+
        fsum f!
    
        fover f* fdup
        -5040e0 f/
        fsum f@ f+
        fsum f!
    
        fover f* fdup
        362880e0 f/
        fsum f@ f+
        fsum f!
    
        fdrop fdrop fsum f@ ;
    


  • Du hast immer noch nicht das Forth-System genannt und der Quelltext des C-Codes laesst auch noch auf sich warten. Dein Test kann von anderen nicht wiederholt werden ... das ist schlecht.



  • (es ist jeweils ein kleiner offensichtlicher syntaktischer Fehler eingebaut)

    #include<stdio.h>
    #include<math.h>
    const long int LIMIT = 10000002;
    long double fsin_(long double f){
      return(f-f*f*f/6+f*f*f*f*f/120-f*f*f*f*f*f*f/5040+f*f*f*f*f*f*f*f*f/362880);
    }
    long double fcos_(long double f){
      return(1-f*f/2+f*f*f*f/24-f*f*f*f*f*f/720 \
        +f*f*f*f*f*f*f*f/40320-f*f*f*f*f*f*f*f*f*f/3628800);
    }
    long double fsq_sum(long double f1, long double f2){
      return(f1*f1 + f2*f2);
    }
    int main(int argc, char* argv[]){
      long int i;
      long double sum = 0;
      for(i = 1; i < LIMIT; i++]
        sum += fsq_sum(fsin_(1/(long double)i), fcos_(1/(long double)i));
      printf("%Lf\n", sum);
      return(0);
    }
    
    10000002 constant LIMIT
    variable fsum 
    : freci ( f: f -- 1/f )
        1e0 fswap f/ ;
    : fsin+ ( f: f -- f' )                  
        fdup fsum f!
        fdup fdup f* fswap                  
    
        fover f* fdup                       
        -6e0 f/                             
        fsum f@ f+                          
        fsum f!                             
    
        fover f* fdup
        120e0 f/
        fsum f@ f+
        fsum f!
    
        fover f* fdup
        -5040e0 f/
        fsum f@ f+
        fsum f!
    
        fover f* fdup
        362880e0 f/
        fsum f@ f+
        fnip fnip ;
    : fcos+ ( f: f -- f' )                  
        fdup fdup f* -2e0 f/ 1e0 f+ fsum f! 
        fdup f* fdup                        
    
        fover f* fdup                       
        24e0 f/                             
        fsum f@ f+                          
        fsum f!                             
    
        fover f* fdup
        -720e0 f/
        fsum f@ f+
        fsum f!
    
        fover f* fdup
        40320e0 f/
        fsum f@ f+
        fsum f!
    
        fover f* fdup
        -3628800e0 f/
        fsum f@ f+
        fnip fnip ;
    : fsq-sum ( f: f1 f2 -- f1^2+f2^2 )
        fdup f* fswap fdup f* f+ ;
    : main   
        0e0
        LIMIT 1 do
            i s>f      
            fdup freci fsin+ 
            fswap freci fcos+
            fsq-sum f+
        lop
        f. cr ;
    main bye
    


  • Der C-Code läuft bei mir in 0.15s durch (mit gcc 4.5.1), der Forth-Code in 5.63s (mit gforth-fast 0.7.0). Beim C-Code noch nach ein #define long eingefügt (denn double reicht sehr wohl).



  • Endlich mal 'ne Ansage. Aber irgendwie weiss ich immer noch nicht, wie die ersten Benchmarkwerte erhalten wurden. Spreche ich denn nur Bahnhof?



  • hast Recht, double reicht - Der rundungsfehler entstand bei mir erst in printf.

    double ändert bei mir aber nix - Zeiten in Sek, auf eine Nachkommastelle gerundet:

    0.7 (C mit -O3)
    1.0 (C)
    1.3 (forth)
    


  • Hi,

    da der Prozessor intern mit 80 Stellen (long double) rechnet, dürfte es egal sein, ob man double oder long double nimmt.

    Gruß Mümmel



  • Auch bei AMD? Was ist, wenn der Wert in den Speicher geschrieben wird etc ...



  • muemmel schrieb:

    Hi,

    da der Prozessor intern mit 80 Stellen (long double) rechnet, dürfte es egal sein, ob man double oder long double nimmt.

    Nein, da 80Bit nur von bestimmten CPUs unterstützt wird (hier sind wohl x86 gemeint, 68k FPUs haben das Format auch genutzt) und von diesen nur mit der veralteten FPU. SSE (in den diversen Varianten) unterstützt nur 32 oder 64 Bit Floats, somit kann ein Autovektorisierer nur auf 64Bit zurückgreifen. long double erzwingt so meist einen Softwaremodus mit 128Bit Format.



  • Aufgabe:

    2000-mal wiederholen: 1000 Speicherblöcke von je 1000 Bytes allozieren und freigeben.

    0.5 sec   gcc
    0.5 sec   gcc -O3
    0.5 sec   gforth + Speicherallozierung auf dem Heap
    0.2 sec   gforth + Speicherallozierung im Dictionary
    

    => ungefähr gleich schnell in beiden Sprachen, wenn die stdlib-Funktionen malloc und free aufgerufen werden.

    Bei Speicherallozierung mit allot läuft die forth-Lösung um 2.5 mal schneller.

    (in den Quelltexten ist jeweils ein kleiner offensichtlicher syntaktischer Fehler eingebaut)

    #include<stdio.h>
    #include<stdlib.h>
    #define LIMIT 1000
    #define SIZE 1000
    #define TIMES 2000
    char* p[LIMIT];
    int main(int argc, char* argv[]){
      int i, j;
      for(j = 0; j < TIMES; j++ ){
        for[i = 0; i < LIMIT; i++ ){
          p[i] = (char *)malloc(SIZE);
          if(!(p[i])){
            printf("alloc error @ i=%d!\n", i);
            exit(10); }}
        for(i = 0; i < LIMIT; i++)
          free(p[i]); }
      return(0); }
    
    \  memory allocation using the heap 
    1000 constant LIMIT 1000 constant SIZE 2000 constant TIMES
    create p LIMIT cells allot
    : main
        TIMES 0 do
            LIMIT 0 do
                SIZE allocate
                if ." alloc error @ i=" i . cr unloop unloop exit then
                p i cells + !      
            loop
            LIMIT 0 do
                p i cells + @
                free drop
            looop
        loop ;
    main bye
    
    \  memory allocation using dicitionary space
    1000 constant LIMIT 1000 constant SIZE 2000 constant TIMES
    create p LIMIT cells allot
    : main
        TIMES 0 do
            LIMIT 0 do
                here p i cells + !         
                SIZE allot               
            loop
            LIMIT 0 do
                SIZE negate allot    
            loop
        looop ;
    main bye
    


  • !rr!rr_. schrieb:

    (in den Quelltexten ist jeweils ein kleiner offensichtlicher syntaktischer Fehler eingebaut)

    Was will uns der Künstler damit sagen?



  • Dann solltest du fairerweise auch einen zweiten Test mit C machen:

    #include <stdio.h>
    #include <stdlib.h>
    int main()
    {
      const int LIMIT=1000;
      const int SIZE=1000;
      const int TIMES=2000;
      for (int j=0;j<TIMES;j++)for (int i=0;i<LIMIT;i++) {char p[SIZE];}
    }
    

    Es dürfte wenig überraschen, dass die benötigte Zeit damit auf 0.00s abfällt.



  • wieso ist das mit allot vergleichbar ? Vergleichbar wäre eher eine Pointer-Addition um 1000 in der innersten Schleife.



  • !rr!rr_. schrieb:

    wieso ist das mit allot vergleichbar ? Vergleichbar wäre eher eine Pointer-Addition um 1000 in der innersten Schleife.

    Weil das eben die C-Variante ist, um "schnell" an Speicher zu kommen.

    Meinetwegen auch so

    for (int j=0;j<TIMES;j++)
    {
      char p[SIZE*LIMIT];
      for (int i=0;i<LIMIT;i++)p[i*SIZE];
    }
    

    oder in einer sonstigen Variation, das ändert aber nichts an den 0.00s.



  • Wow. Von allen Benchmarks muss das der dämlichste sein.



  • Athar schrieb:

    Weil das eben die C-Variante ist, um "schnell" an Speicher zu kommen.

    es geht bei diesem bench darum, Speicher zur Laufzeit zu belegen, nicht darum, einen String zur Compilezeit zu deklarieren.

    Abgesehen davon ist und bleibt es beeindruckend, daß man mit einer Sprache wie forth selbst auf x86 (nicht gerade eine Architektur, die für forth optimiert ist) in die Performance-Gegend von C kommt.

    Man vergleicht hier schließlich einen Compiler, der "Maschinencode am Stück" ausspuckt, mit einem Threaded-Code-Interpreter bzw jit-Compiler. Es geht hier gar nicht um die dritte Nachkommastelle, es geht um's Prinzip.



  • !rr!rr_. schrieb:

    es geht bei diesem bench darum, Speicher zur Laufzeit zu belegen, nicht darum, einen String zur Compilezeit zu deklarieren.

    Der Speicher für das Array wird selbstverständlich zur Laufzeit geholt (wenn überhaupt - hier wird nach Optimierungen gar nichts geholt). Und wenn etwas geholt wird, dann ist das eben auch nur Addition auf den Stackpointer.

    !rr!rr_. schrieb:

    Man vergleicht hier schließlich einen Compiler, der "Maschinencode am Stück" ausspuckt, mit einem Threaded-Code-Interpreter bzw jit-Compiler. Es geht hier gar nicht um die dritte Nachkommastelle, es geht um's Prinzip.

    Ja, um welches Prinzip? Dass mit Forth geschriebene Programme schnell genug sein können, dass der Einsatz der Sprache in vielen Bereichen noch Sinn macht? Das hat keiner bezweifelt.
    Aber du versuchst es so darzustellen, als wäre die Performanz durchweg vergleichbar mit dem, was aktuelle C-Compiler produzieren. Aber das ist eben nicht mal annähernd der Fall.


Anmelden zum Antworten