Warum ist Valarray so langsam?

Lesezeit: 13 Minuten

Benutzer-Avatar
hängen

Ich versuche, Valarray zu verwenden, da es MATLAB beim Betrieb von Vektoren und Matrizen sehr ähnlich ist. Ich habe zuerst eine Leistungsprüfung durchgeführt und festgestellt, dass valarray die im Buch deklarierte Leistung nicht erreichen kann Programmiersprache C++ von Stroustrup.

Das Testprogramm führte tatsächlich eine 5-Millionen-Multiplikation von Doubles durch. Ich dachte, dass c = a*b zumindest vergleichbar wäre mit dem for Loop Double Typ Element Multiplikation, aber ich bin völlig falsch. Ich habe es auf mehreren Computern und Microsoft Visual C++ 6.0 und Visual Studio 2008 versucht.

Übrigens habe ich auf MATLAB mit folgendem Code getestet:

len = 5*1024*1024;
a = rand(len, 1);
b = rand(len, 1);
c = zeros(len, 1);
tic;
c = a.*b;
toc;

Und das Ergebnis ist 46 ms. Diese Zeit ist nicht hochpräzise; es funktioniert nur als Referenz.

Der Code lautet:

#include <iostream>
#include <valarray>
#include <iostream>
#include "windows.h"

using namespace std;
SYSTEMTIME stime;
LARGE_INTEGER sys_freq;

double gettime_hp();

int main()
{
    enum { N = 5*1024*1024 };
    valarray<double> a(N), b(N), c(N);
    QueryPerformanceFrequency(&sys_freq);
    int i, j;
    for (j=0 ; j<8 ; ++j)
    {
        for (i=0 ; i<N ; ++i)
        {
            a[i] = rand();
            b[i] = rand();
        }

        double* a1 = &a[0], *b1 = &b[0], *c1 = &c[0];
        double dtime = gettime_hp();
        for (i=0 ; i<N ; ++i)
            c1[i] = a1[i] * b1[i];
        dtime = gettime_hp()-dtime;
        cout << "double operator* " << dtime << " ms\n";

        dtime = gettime_hp();
        c = a*b ;
        dtime = gettime_hp() - dtime;
        cout << "valarray operator* " << dtime << " ms\n";

        dtime = gettime_hp();
        for (i=0 ; i<N ; ++i)
            c[i] = a[i] * b[i];
        dtime = gettime_hp() - dtime;
        cout << "valarray[i] operator* " << dtime<< " ms\n";

        cout << "------------------------------------------------------\n";
    }
}

double gettime_hp()
{
    LARGE_INTEGER tick;
    extern LARGE_INTEGER sys_freq;
    QueryPerformanceCounter(&tick);
    return (double)tick.QuadPart * 1000.0 / sys_freq.QuadPart;
}

Die Laufergebnisse: (Freigabemodus mit maximaler Geschwindigkeitsoptimierung)

double operator* 52.3019 ms
valarray operator* 128.338 ms
valarray[i] operator* 43.1801 ms
------------------------------------------------------
double operator* 43.4036 ms
valarray operator* 145.533 ms
valarray[i] operator* 44.9121 ms
------------------------------------------------------
double operator* 43.2619 ms
valarray operator* 158.681 ms
valarray[i] operator* 43.4871 ms
------------------------------------------------------
double operator* 42.7317 ms
valarray operator* 173.164 ms
valarray[i] operator* 80.1004 ms
------------------------------------------------------
double operator* 43.2236 ms
valarray operator* 158.004 ms
valarray[i] operator* 44.3813 ms
------------------------------------------------------

Debugging-Modus mit gleicher Optimierung:

double operator* 41.8123 ms
valarray operator* 201.484 ms
valarray[i] operator* 41.5452 ms
------------------------------------------------------
double operator* 40.2238 ms
valarray operator* 215.351 ms
valarray[i] operator* 40.2076 ms
------------------------------------------------------
double operator* 40.5859 ms
valarray operator* 232.007 ms
valarray[i] operator* 40.8803 ms
------------------------------------------------------
double operator* 40.9734 ms
valarray operator* 234.325 ms
valarray[i] operator* 40.9711 ms
------------------------------------------------------
double operator* 41.1977 ms
valarray operator* 234.409 ms
valarray[i] operator* 41.1429 ms
------------------------------------------------------
double operator* 39.7754 ms
valarray operator* 234.26 ms
valarray[i] operator* 39.6338 ms
------------------------------------------------------

  • Hast du die ausführbare Datei ausgeführt? oder haben Sie es in einem Debugger (über Visual Studio) versucht?

    – Yochai Timmer

    27. Juli 2011 um 20:34 Uhr


  • Welche Optimierungseinstellungen verwendest du?

    – Alan Stokes

    27. Juli 2011 um 20:37 Uhr

  • Es zeigt weder im Debugger noch in der exe einen Unterschied

    – shangen

    27. Juli 2011 um 20:38 Uhr

  • VC6? Wirklich? Es ist 13 Jahre alt und älter als der Standard.

    – Alan Stokes

    27. Juli 2011 um 20:38 Uhr

  • Auf GCC 4.6.1 mit -flto -O3 -march=native -std=c++0xerhalte ich für alle drei Fälle nahezu identische Leistung, mit einem winzigen Anstieg vom ersten zum dritten.

    – Kerrek SB

    27. Juli 2011 um 20:56 Uhr


Benutzer-Avatar
Paul R

Ich habe es gerade auf einem Linux x86-64-System (Sandy Bridge-CPU) ausprobiert:

gcc 4.5.0:

double operator* 9.64185 ms
valarray operator* 9.36987 ms
valarray[i] operator* 9.35815 ms

Intel-ICC 12.0.2:

double operator* 7.76757 ms
valarray operator* 9.60208 ms
valarray[i] operator* 7.51409 ms

In beiden Fällen habe ich gerade verwendet -O3 und keine anderen optimierungsbezogenen Flags.

Es sieht so aus, als ob der MS C++-Compiler und/oder die Valarray-Implementierung scheiße sind.


Hier ist der für Linux modifizierte OP-Code:

#include <iostream>
#include <valarray>
#include <iostream>
#include <ctime>

using namespace std ;

double gettime_hp();

int main()
{
    enum { N = 5*1024*1024 };
    valarray<double> a(N), b(N), c(N) ;
    int i,j;
    for(  j=0 ; j<8 ; ++j )
    {
        for(  i=0 ; i<N ; ++i )
        {
            a[i]=rand();
            b[i]=rand();
        }

        double* a1 = &a[0], *b1 = &b[0], *c1 = &c[0] ;
        double dtime=gettime_hp();
        for(  i=0 ; i<N ; ++i ) c1[i] = a1[i] * b1[i] ;
        dtime=gettime_hp()-dtime;
        cout << "double operator* " << dtime << " ms\n" ;

        dtime=gettime_hp();
        c = a*b ;
        dtime=gettime_hp()-dtime;
        cout << "valarray operator* " << dtime << " ms\n" ;

        dtime=gettime_hp();
        for(  i=0 ; i<N ; ++i ) c[i] = a[i] * b[i] ;
        dtime=gettime_hp()-dtime;
        cout << "valarray[i] operator* " << dtime<< " ms\n" ;

        cout << "------------------------------------------------------\n" ;
    }
}

double gettime_hp()
{
    struct timespec timestamp;

    clock_gettime(CLOCK_REALTIME, &timestamp);
    return timestamp.tv_sec * 1000.0 + timestamp.tv_nsec * 1.0e-6;
}

  • Schön – nur als Referenz, könnten Sie die im Build verwendeten Optionen hinzufügen (ich kann heute Abend mit diesem Zeug herumspielen …)

    – Michael Burr

    27. Juli 2011 um 22:27 Uhr

  • +1. Ich habe diesen Benchmark auf der libc++-Implementierung ausgeführt. Es war nicht so langsam wie MS, aber nicht so schnell wie gcc (es war ungefähr die gleiche Geschwindigkeit, die Sie für ICC angeben). Es stellte sich heraus, dass mir ein Schlüsselzuweisungsoperator in der Ausdrucksvorlagen-Engine fehlte. Das hinzugefügt. Jetzt ist libc++ so schnell wie gcc. An das OP: Danke für den Speedtest! (+1 auf die Frage auch) 🙂

    – Howard Hinnant

    27. Juli 2011 um 23:24 Uhr

  • Vielen Dank an beide – ich habe einen Hinweis zu Compiler-Schaltern hinzugefügt (just -O3 in beiden Fällen) und auch den für Linux modifizierten OP-Code angehängt, den ich für diese Tests verwendet habe.

    – PaulR

    28. Juli 2011 um 6:55 Uhr

Ich vermute, dass der Grund c = a*b ist so viel langsamer als das Ausführen der Operationen Element für Element

template<class T> valarray<T> operator*
    (const valarray<T>&, const valarray<T>&);

Der Operator muss Speicher zuweisen, in den das Ergebnis eingefügt werden soll, und gibt es dann als Wert zurück.

Selbst wenn eine “Auslagerungsoptimierung” verwendet wird, um die Kopie durchzuführen, hat diese Funktion immer noch den Overhead von

  • Zuweisen des neuen Blocks für das Ergebnis valarray
  • Initialisieren des Neuen valarray (es ist möglich, dass dies wegoptimiert wird)
  • die Ergebnisse in das Neue übertragen valarray
  • im Gedächtnis nach Neuem blättern valarray wie es initialisiert oder mit Ergebniswerten gesetzt wird
  • Altes auflösen valarray die durch das Ergebnis ersetzt wird

  • Ich habe gerade gesucht, es gibt tatsächlich eine Referenz zurück: template inline valarray<_Ty>& operator*=(valarray<_Ty>& _L, const _Ty& _R) {_VALGOP2(*= _R); }

    – shangen

    27. Juli 2011 um 20:57 Uhr

  • Es gibt zwei Unterschiede in der Erklärung, die Sie im obigen Kommentar gepostet haben, von dem, was in dem in der Frage geposteten Code verwendet würde: 1) operator*= ist anders als zu verwenden operator*() gefolgt von operator=()und 2) das ist die Deklaration für die *= Operator, der ein skalares Argument verwendet, um die zu multiplizieren valarray durch

    – Michael Burr

    27. Juli 2011 um 21:02 Uhr


  • Michael, der Analyse zufolge haben wir keine Möglichkeit, das Valarray zu verwenden, wenn Leistung benötigt wird. Allerdings ist diese Klasse laut Buch speziell auf die Leistungssteigerung ausgelegt. Würdest du mir ein paar Punkte geben? Gibt es eine andere Methode, mit der ich Arrays als Ganzes genauso behandeln kann wie Valarray in C++? Vielen Dank

    – shangen

    27. Juli 2011 um 21:13 Uhr

  • @Michael: Sie haben vielleicht Recht, aber sehen Sie sich meine Linux-Benchmarks in einer separaten Antwort an – es scheint sicherlich, dass die Valarray-Leistung einer geraden Schleife nicht wesentlich unterlegen sein muss, vorausgesetzt, Sie verwenden einen anständigen Compiler und eine Valarray-Implementierung.

    – PaulR

    27. Juli 2011 um 21:27 Uhr


  • Newsflash: Valarray-Arithmetik ist erlaubt, aber nicht erforderlich Ausdrucksvorlagen (en.wikipedia.org/wiki/Expression_templates). Die Verwendung von Ausdrucksvorlagen kann Temporäre im Problem des OP vollständig eliminieren und somit die Heap-Zuweisung und -Freigabe für den Ausdruck vollständig eliminieren c = a*b. Es ist offensichtlich, dass gcc dies tut (und eine leicht korrigierte libc++), und dass MS C++ dies nicht tut.

    – Howard Hinnant

    27. Juli 2011 um 23:47 Uhr

Benutzer-Avatar
Leo Bellantoni

Der springende Punkt bei Valarray ist es, auf Vektormaschinen schnell zu sein, was x86-Maschinen einfach nicht sind.

Eine gute Implementierung auf einer Nicht-Vektor-Maschine sollte in der Lage sein, die Leistung zu erreichen, die Sie mit etwas wie erhalten

for (i=0; i < N; ++i) 
    c1[i] = a1[i] * b1[i];

und ein schlechter natürlich nicht. Wenn die Hardware nicht etwas enthält, um die Parallelverarbeitung zu beschleunigen, wird das dem Besten, was Sie tun können, ziemlich nahe kommen.

Benutzer-Avatar
hängen

Ich habe es endlich durch die verzögerte Auswertung geschafft. Der Code kann hässlich sein, da ich gerade erst anfange, diese fortgeschrittenen C++-Konzepte zu lernen.

Hier ist der Code:

#include <iostream>
#include <valarray>
#include <iostream>
#include "windows.h"

using namespace std;
SYSTEMTIME stime;
LARGE_INTEGER sys_freq;

double gettime_hp();

// To improve the c = a*b (it will generate a temporary first, assigned to 'c' and delete the temporary.
// Which causes the program really slow
// The solution is the expression template and let the compiler to decide when all the expression is known.


// Delayed evaluation
//typedef valarray<double> Vector;
class Vector;

class VecMul
{
    public:
        const Vector& va;
        const Vector& vb;
        //Vector& vc;
        VecMul(const Vector& v1, const Vector& v2): va(v1), vb(v2) {}
        operator Vector();
};

class Vector:public valarray<double>
{
    valarray<double> *p;

    public:
        explicit Vector(int n)
        {
            p = new valarray<double>(n);
        }
        Vector& operator = (const VecMul &m)
        {
            for(int i=0; i<m.va.size(); i++)
                (*p)[i] = (m.va)[i]*(m.vb)[i]; // Ambiguous
            return *this;
        }
        double& operator[](int i) const {return (*p)[i];} //const vector_type[i]
        int size()const {return (*p).size();}
};


inline VecMul operator*(const Vector& v1, const Vector& v2)
{
    return VecMul(v1, v2);
}


int main()
{
    enum {N = 5*1024*1024};
    Vector a(N), b(N), c(N);
    QueryPerformanceFrequency(&sys_freq);
    int i, j;
    for (j=0 ; j<8 ; ++j)
    {
        for (i=0 ; i<N ; ++i)
        {
            a[i] = rand();
            b[i] = rand();
        }

        double* a1 = &a[0], *b1 = &b[0], *c1 = &c[0];
        double dtime = gettime_hp();
        for (i=0 ; i<N ; ++i)
            c1[i] = a1[i] * b1[i];
        dtime = gettime_hp()-dtime;
        cout << "double operator* " << dtime << " ms\n";

        dtime = gettime_hp();
        c = a*b;
        dtime = gettime_hp()-dtime;
        cout << "valarray operator* " << dtime << " ms\n";

        dtime = gettime_hp();
        for (i=0 ; i<N ; ++i)
            c[i] = a[i] * b[i];
        dtime = gettime_hp() - dtime;
        cout << "valarray[i] operator* " << dtime << " ms\n";

        cout << "------------------------------------------------------\n";
    }
}

double gettime_hp()
{
    LARGE_INTEGER tick;
    extern LARGE_INTEGER sys_freq;
    QueryPerformanceCounter(&tick);
    return (double)tick.QuadPart*1000.0/sys_freq.QuadPart;
}

Das laufende Ergebnis in Visual Studio ist:

double operator* 41.2031 ms
valarray operator* 43.8407 ms
valarray[i] operator* 42.49 ms

Benutzer-Avatar
Welpe

Ich kompiliere in Version x64, Visual Studio 2010. Ich habe Ihren Code sehr leicht geändert:

    double* a1 = &a[0], *b1 = &b[0], *c1 = &c[0];
    double dtime = gettime_hp();
    for (i=0 ; i<N ; ++i)
        a1[i] *= b1[i];
    dtime = gettime_hp() - dtime;
    cout << "double operator* " << dtime << " ms\n";

    dtime = gettime_hp();
    a *= b;
    dtime = gettime_hp() - dtime;
    cout << "valarray operator* " << dtime << " ms\n";

    dtime = gettime_hp();
    for (i=0 ; i<N ; ++i)
        a[i] *= b[i];
    dtime = gettime_hp() - dtime;
    cout << "valarray[i] operator* " << dtime<< " ms\n";

    cout << "------------------------------------------------------\n" ;

Hier können Sie sehen, dass ich *= anstelle von verwendet habe c = a * b. In moderneren mathematischen Bibliotheken werden sehr komplexe Ausdrucksvorlagenmechanismen verwendet, die dieses Problem beseitigen. In diesem Fall habe ich tatsächlich etwas schnellere Ergebnisse von valarray erhalten, obwohl das wahrscheinlich nur daran liegt, dass sich der Inhalt bereits in einem Cache befand. Der Overhead, den Sie sehen, sind einfach redundante Temporäre und nichts, was valarray eigen ist, insbesondere – Sie würden das gleiche Verhalten mit etwas wie sehen std::string.

  • Ich habe deine Ergebnisse verifiziert. Diese Änderung ist jedoch keine geringfügige Änderung. Viele zusammengesetzte Ausdrücke können nicht immer mit *=, += /= ausgeführt werden

    – shangen

    27. Juli 2011 um 22:19 Uhr

  • @shangping: Wenn Sie in diesem Fall ein neues Ergebnis-Array für jede der benötigten temporären Variablen zuweisen, würden Sie eine ähnliche Verlangsamung für sehen double wie für valarray.

    – Welpe

    28. Juli 2011 um 8:58 Uhr

  • “In moderneren mathematischen Bibliotheken werden sehr komplexe Mechanismen für Ausdrucksvorlagen verwendet, die dieses Problem beseitigen.” Und das auch in hochwertigen Umsetzungen von std::valarray.

    – Jonathan Wakely

    5. April 2018 um 23:38 Uhr

Benutzer-Avatar
Peter Mortensen

Ich denke, die Antwort von Michael Burr ist richtig. Und vielleicht können Sie einen virtuellen Typ als Typ für den Rückgabewert des Operators erstellen +und laden Sie eine andere neu operator= für diesen virtuellen Typ wie operator=(virtual type& v){&valarray=&v;v=NULL;} (grob gesprochen).

Natürlich ist es schwierig, die Idee auf Valarray umzusetzen. Aber wenn Sie eine neue Klasse erstellen, können Sie diese Idee ausprobieren. Und dann die Effizienz für operator+ ist fast das gleiche wie operator+=.

  • Ich habe deine Ergebnisse verifiziert. Diese Änderung ist jedoch keine geringfügige Änderung. Viele zusammengesetzte Ausdrücke können nicht immer mit *=, += /= ausgeführt werden

    – shangen

    27. Juli 2011 um 22:19 Uhr

  • @shangping: Wenn Sie in diesem Fall ein neues Ergebnis-Array für jede der benötigten temporären Variablen zuweisen, würden Sie eine ähnliche Verlangsamung für sehen double wie für valarray.

    – Welpe

    28. Juli 2011 um 8:58 Uhr

  • “In moderneren mathematischen Bibliotheken werden sehr komplexe Mechanismen für Ausdrucksvorlagen verwendet, die dieses Problem beseitigen.” Und das auch in hochwertigen Umsetzungen von std::valarray.

    – Jonathan Wakely

    5. April 2018 um 23:38 Uhr

Benutzer-Avatar
Peter Mortensen

Hmm..ich habe es getestet Blitz++ und es ist dasselbe wie valarray … Und außerdem der Blitz ++ [] Bediener ist sehr langsam.

#include <blitz/array.h>
#include <iostream>

#ifdef WIN32
#include "windows.h"
LARGE_INTEGER sys_freq;
#endif

#ifdef LINUX
<ctime>
#endif

using namespace std;
SYSTEMTIME stime;

__forceinline double gettime_hp();
double gettime_hp()
{
    #ifdef WIN32
        LARGE_INTEGER tick;
        extern LARGE_INTEGER sys_freq;
        QueryPerformanceCounter(&tick);
        return (double)tick.QuadPart * 1000.0 / sys_freq.QuadPart;
    #endif

    #ifdef LINUX
        struct timespec timestamp;

        clock_gettime(CLOCK_REALTIME, &timestamp);
        return timestamp.tv_sec * 1000.0 + timestamp.tv_nsec * 1.0e-6;
    #endif
}
BZ_USING_NAMESPACE(blitz)

int main()
{
    int N = 5*1024*1024;

    // Create three-dimensional arrays of double
    Array<double, 1> a(N), b(N), c(N);

    int i, j;

    #ifdef WIN32
        QueryPerformanceFrequency(&sys_freq);
    #endif

    for (j=0 ; j<8 ; ++j)
    {
        for (i=0 ; i<N ; ++i)
        {
            a[i] = rand();
            b[i] = rand();
        }

        double* a1 = a.data(), *b1 = b.data(), *c1 = c.data();
        double dtime = gettime_hp();
        for (i=0 ; i<N ; ++i)
            c1[i] = a1[i] * b1[i];
        dtime = gettime_hp() - dtime;
        cout << "double operator* " << dtime << " ms\n";

        dtime = gettime_hp();
        c = a*b;
        dtime = gettime_hp() - dtime;
        cout << "blitz operator* " << dtime << " ms\n";

        dtime = gettime_hp();
        for (i=0 ; i<N ; ++i)
            c[i] = a[i] * b[i];
        dtime = gettime_hp() - dtime;
        cout << "blitz[i] operator* " << dtime<< " ms\n";

        cout << "------------------------------------------------------\n";
    }
}

  • … und was war das Ergebnis?

    – Menge

    11. August 2014 um 4:50 Uhr


1015610cookie-checkWarum ist Valarray so langsam?

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

Privacy policy