Laufzeit sin mit march=native komisch



  • Ich habe mal geschaut wie lange die Laufzeit eines Programmes mit der "g++ -march=native" Option. In den Kommentaren steht erst normale Zeit in Sekunden, dann mit der Option und dann, falls ein Faktor drin ist noch als double (wieder normal und mit der Option)

    #include <iostream>
    #include <cmath>
    
    int main(){
    
        const int hm = 10000;
        double sum = 0;
        float fac = 0.815; //oder double
        for (int i = 0; i<hm;i++)
            for (int j = 0; j<hm;j++)            //normal   -march=native  | double:    native
                //sum +=   sin((j+i));           // 9.0          3.5
                //sum +=   sin((j+i)*fac);       // 9.0          3.7          3.5        3.5
                //sum +=   std::sin((j+i));      // 9.5          3.8
                //sum +=   std::sin((j+i)*fac);  // 1.7          7.2          9.0        3.5
    
                //sum +=   exp((j+i));           // 6.2         10.8
                //sum +=   exp((j+i)*fac);       // 6.4         11.0          6.4       10.9
                //sum +=   std::exp((j+i));      // 6.0         11.0
                //sum +=   std::exp((j+i)*fac);  // 1.9          6.3          6.4       10.9
    
        std::cout << sum <<std::endl;
    }
    

    Kann mir jemand sagen warum "march=native" manchmal besser ist und manchmal nicht? std::sin besonders interessant mit float ist ohne besser mit double ist mit besser. Als Alternative zum Faktor kann man es auch einfach casten.


  • Mod

    Zuallererst die obligatorische Frage: Wie hast du gemessen? Aussagekräftige Messung ist nicht direkt trivial. Sind Optimierungsflags mitgegeben worden?

    Und dazu sollte natürlich erwähnt werden, dass ein Optimizer keine Garantien bezüglich Laufzeiten macht, sondern nur über die Semantik des produzierten Codes. Ich kenne keine Statistiken, aber es wäre nicht das erste mal, dass eine Optimierung sich negativ auswirkt (bzw. unter gewissen Umständen, oder dass es so scheint).



  • Ich kann das hier mit Intel(R) Core(TM) i5-3337U CPU @ 1.80GHz reproduzieren, und zwar sowohl mit g++5.4 als auch clang-3.9.

    Zu Arcoths Frage: kompiliert und Zeit gemessen habe ich mit

    g++ -O3 (-march=native) archperformance.cpp && time ./a.out
    

    Wobei man den Unterschied zwischen 1,5s und 9s bzw. zwischen 4s und 12s auch komplett ohne "time" merkt. Diese Messung ist trivial. Da der Unterschied so groß ist, ist hier auch kein besonderes Messtool nötig und 1x messen reicht auch aus.

    Ich habe raus bei:

    Zeit=user, nMessungen=1
    Zeiten: float / float,native ; double / double,native
    
    // std::sin((j+i)*fac);
    g++:   1,52s / 8,95s ; 12,03s / 4,17s
    clang: 1,45s / 8,98s ; 11,98s / 4,27s
    


  • Hallo

    Meine Messungen für sum += std::sin((j+i)*fac); :

    Compiler  float   float/native  double  double/native
    g++       0.884s  0.888s        1.826s  1.828s
    clang     0.909s  1.247s        1.807s  1.777s
    

    CPU: i7-6800k @ 4.0GHz
    GCC: 7.1.1
    Clang: 4.0.1

    Kompiliert habe ich jeweils mit -O3 und gemessen habe ich auch mit time.

    Scheint, als ob das ein GCC-Bug ist, der in der neueren Version gefixed wurde.

    LG



  • Hi, ich habe ähnlich wie wob nur:

    g++ (-march=native) archperformance.cpp && time ./a.out
    

    gemacht (g++ 5.4). Mit den Optionen O3 oder "std=c++xx" hatte ich auch rumprobiert aber die hatten keine signifikanten Anderungen bewirkt.

    Fytch schrieb:

    Scheint, als ob das ein GCC-Bug ist, der in der neueren Version gefixed wurde.

    Oder bei neueren CPU's. Hmm, apt-get geht bei mir nur bis gcc-6-base und g++-5.



  • Kann ich dem Kompiler sagen, dass er bei einer Codezeile (sin) march=native nicht machen soll?



  • std::sin((j+i)*fac):
       float / float,native ; double / double,native
    
    g++:   5,21s / 4,23s ; 5,08s / 4,23s
    clang: 5,24s / 4,97s ; 5,08s / 4,36s
    

    CPU: i5-6200U
    GCC: 5.4.0
    Clang: 3.8

    Spaßeshalber noch in einer VM mit GCC7 und Clang4 ausprobiert:

    std::sin((j+i)*fac):
       float / float,native ; double / double,native
    
    g++:   2,90s / 2,87s ; 3,04s / 3,13s
    clang: 3,13s / 2,96s ; 2,99s / 3,07s
    

    GCC: 7.1.1
    Clang: 4.0.1



  • Also scheinbar doch die CPU. Danke für die ganzen Tests. Mit clang 4.0.0 bekomme ich die selben Werte wie mit g++5.4.



  • mit g++7.1 auch so.

    sum +=   std::sin((j+i)*fac);  // 1.7          7.2          9.0        3.5    //g++ 5.4
    sum +=   std::sin((j+i)*fac);  // 2.8          7.0          9.0        3.2    //g++ 7.1
    


  • Trotz allem wundert mich bei dem Code doch ein wenig, dass hier überhaupt eine Zeit gemessen werden kann.
    Meine Erfahrungen mit subjektiv komplizierteren Berechnungen hätten mich jetzt darauf tippen lassen, dass hier
    ohne irgendwelche volatile -Tricks fast alle Compiler nur Code erzeugen, der das Endergebnis ausgibt.

    Allerdings sind Compiler-Optimierungen auch nie wirklich gut vorhersehbar 😉


  • Mod

    Hier scheint eine spezifische Eigenheit von SandyBridge/IvyBridge-Prozessoren zuzuschlagen.

    #include <iostream>
    #include <cmath>
    
    int main(){
    
        const int hm = 10000;
        double sum = 0;
        T fac = 0.815;
        for (int i = 0; i<hm;i++)
            for (int j = 0; j<hm;j++)
                sum += [&]() {
                    auto res = F;
    #if VZEROUPPER
                    asm volatile("vzeroupper":::"memory");
    #endif
                    return res;
                }();
        std::cout << sum << '\t';
    }
    

    mit

    for y in "sin((j+i))" "sin((j+i)*fac)" "std::sin((j+i))" "std::sin((j+i)*fac)" "exp((j+i))" "exp((j+i)*fac)" "std::exp((j+i))" \
    "std::exp((j+i)*fac)"; do for x in float double; do for a in "" "-march=native"; do for z in 0 1; do echo $y $x $a $z; \
    clang++ -std=c++1z -O3 $a main.cpp -DT=$x -DF=$y -DVZEROUPPER=$z && /usr/bin/time -f "%e" ./a.out; done; done; done; done
    

    Mit vzeroupper verschwinden die Unterschiede zwischen mit und ohne "-march=native", z.T. sind sogar beide Varianten schneller.

    Der Code, den Compiler erzeugt, je nachdem, ob avx erlaubt ist oder nicht, ist praktisch identisch, mit -mavx benutzt der Compiler die VEX-kodierten Pendants zum normalen SSE-Code. Offenbar hinterlassen die transzendenten Funktionen (aus glibc) z.T. Rückstände in den oberen Häften der ymm-Register, was bei den angesprochenen Prozessoren zu Verzögerungen führen kann.



  • Oh, Danke für die informative Antwort.
    Könnte man statt dessen die sin/exp Funktion auf allen Registern rechnen lassen? (#pragma omp simd hatte keine Wirkung gezeigt). Wenn man das tut müsste der Unterschied dann nicht auftreten, oder?

    statt

    sum += [&]() {
                    auto res = F;
    #if VZEROUPPER
                    asm volatile("vzeroupper":::"memory");
    #endif
                    return res;
                }();
    

    konnte ich auch einfach

    {
    sum+=F;
    #if VZEROUPPER
                    asm volatile("vzeroupper":::"memory");
    #endif
    }
    

    (alternativ auch erst VZEROUPPER und dann sum+=F)
    schreiben. Macht das einen Unterschied?

    Wenn man das nutzt, geht es für Sandy Bridge, Ivy Bridge, Haswell, und Broadwell aber scheint für Knights Landing wiederum langsammer zu sein...
    (http://www.agner.org/optimize/blog/read.php?i=789)


  • Mod

    Sinativ schrieb:

    Könnte man statt dessen die sin/exp Funktion auf allen Registern rechnen lassen?

    Heisst was?

    Sinativ schrieb:

    Macht das einen Unterschied?

    Kaum. Du kannst es auch in die äußere Schleife verschieben. Offenbar hinterlassen die Funktionen nur in seltenen Fällen Mist in den ymm-Registern.



  • Heisst was?

    Das es 2,4 bzw. 8 Rechnungen gleichzeitig macht (wie bei mult und add möglich) und alle Register (einschließlich ymm-Registern) genutzt werden, so dass die automatisch überschrieben werden.


Log in to reply