zespolone z klasą, a szybkość

0

Witam,

Zastanawiam się czy zainwestować trochę w klasy, ekspertem nie jestem, zawsze niskopoziomowo.
Przy większej ilości operacjach na liczbach zespolonych, klasy z przeciążonymi operatorami byłyby super, jednak przeszkodą jest prędkość, albo moja niewiedza:

// przyklad
#include <QDebug>
#include <QElapsedTimer>

QElapsedTimer myTimer;

void tic(){
    myTimer.restart();
}

void toc(const QString &text){
    qDebug()<<text<<myTimer.nsecsElapsed();
}


class QComplex{
public:
    double re;
    double im;

    QComplex(double re = 0, double im = 0){
        this->re = re;
        this->im = im;
    }

    inline double operator +(double val){
        return re + val;
    }

    inline QComplex operator +(const QComplex &cpx){
        return QComplex(re + cpx.re, im + cpx.im);
    }
};


// ladnie ;)
void cpx_speedtest(){
    QComplex a(1, 1), b(2, 2), c;
    tic();
    for(int i=0; i<10000; ++i){
        c = a+b;
    }
    toc("Ladnie: ");
}


// szybko ;)
void normal_speedtest(){
    double a_re = 1;
    double a_im = 1;
    double b_re = 2;
    double b_im = 2;
    double c_re = 0;
    double c_im = 0;
    tic();
    for(int i=0; i<10000; ++i){
        c_re = a_re + b_re;
        c_im = a_im + b_im;
    }
    toc("Szybko: ");
}

int main()
{
    cpx_speedtest();
    normal_speedtest();
    return 0;
}

"Ladnie: " 130535 
"Szybko: " 30172 

Da się to jakoś podrasować? Boli mnie że przy dodawaniu ciągle jest tworzona i niszczona klasa...
Może odpuścić i dziubać zawijasy w C?

1

Dla kompilacji z optymalizacją gcc -O2 oraz -O3 (razem z wymuszeniem żeby optymalizator nie usunął tych pętli) mam praktycznie zero różnicy w czasie wykonania, ba opcja "ładnie" działa nawet szybciej.

// przyklad
#include <iostream>
#include <windows.h>
using namespace std;

int tic(){
    return GetTickCount();
}

void toc(const string &text, int prev){
    cout<<text<<" "<<tic()-prev<<endl;
}

class QComplex{
public:
    double re;
    double im;

    QComplex(double re = 0, double im = 0){
        this->re = re;
        this->im = im;
    }

    inline const QComplex operator+(const QComplex &cpx) const{
        return QComplex(re + cpx.re, im + cpx.im);
    }
};


// ladnie ;)
void cpx_speedtest(){
    QComplex a(1, 1), b(2, 2), c;
    int prev = tic();
    for(int i=0; i<100000000; ++i){
        c = a+b;
        if(i%10000000==0){
            cout<<c.re<<" "<<c.im<<endl;
        }
    }
    toc("Ladnie: ", prev);
}


// szybko ;)
void normal_speedtest(){
    double a_re = 1;
    double a_im = 1;
    double b_re = 2;
    double b_im = 2;
    double c_re = 0;
    double c_im = 0;
    int prev = tic();
    for(int i=0; i<100000000; ++i){
        c_re = a_re + b_re;
        c_im = a_im + b_im;
        if(i%10000000==0){
            cout<<c_re<<" "<<c_im<<endl;
        }
    }
    toc("Szybko: ", prev);
}

int main()
{
    normal_speedtest();
    cpx_speedtest();
    return 0;
}

Więc zamiast kombinować powinieneś zostawić niskopoziomowe dziubanie kompilatorowi ;)

0

Dzięki wielkie trochę dostałem oświecenia ;), ale z tego co widzę wszystko trzeba przetestować ;)
U mnie wystarczyło zmienić projekt z Debug na Release

#include <QDebug>
#include <QVector>

#include <QElapsedTimer>

QElapsedTimer myTimer;

void tic(){
    myTimer.restart();
}

void toc(const QString &text){
    qDebug()<<text<<myTimer.nsecsElapsed();
}

QVector<double> test_high_level(QVector<double> &a, QVector<double> &b){
    QVector<double> c(a.size());
    for(int i=0; i<b.size(); ++i){
        c[i] = a[i] + b[i];
    }
    return c;
}

void test_low_level1(QVector<double> &a, QVector<double> &b, QVector<double> &c){
    for(int i=0; i<b.size(); ++i){
        c[i] = a[i] + b[i];
    }
}

void test_low_level2(QVector<double> &a, QVector<double> &b, QVector<double> &c){
    double * A = a.data();
    double * B = b.data();
    double * C = c.data();
    for(int i=0; i<b.size(); ++i){
        C[i] = A[i] + B[i];
    }
}

int main()
{
    QVector<double> a(1000000);
    QVector<double> b(1000000);
    QVector<double> c(1000000);


    // przygotowanie danych
    for(int i=0; i<a.size(); ++i){
        a[i] = i;
        b[i] = 2*i;
    }
    qDebug() << a[123] << b[123];

    tic();
    c = test_high_level(a, b);
    toc("High level");
    qDebug() << c[1];

    tic();
    test_low_level1(a, b, c);
    toc("Low level1");
    qDebug() << c[1];

    tic();
    test_low_level2(a, b, c);
    toc("Low level2");
    qDebug() << c[1];


    return 0;
}

Tutaj kolejna ciekawostka:

Debug:

123 246 
"High level" 58075647 
3 
"Low level1" 51003517 
3 
"Low level2" 9021841 
3 

Release:

123 246 
"High level" 14684696 
3 
"Low level1" 8265385 
3 
"Low level2" 7773901 
3 

Proszę o interpretację wyników, czy:
1. Jest sens "konwertować" z QVector<double> na double?*

double * A = a.data();

Pracowałem ciągle na "Debug" i nie pomyślałem że na Release będzie szybciej. Wszędzie w programie stosowałem takiej zamiany.
2. Zwracać dane wynikowe przez return, czy przez referencje?

QVector<double> test_high_level(QVector<double> &a, QVector<double> &b);
// kontra
void test_low_level1(QVector<double> &a, QVector<double> &b, QVector<double> &c);

Widać że przez return jest wolniej, pisałem o tym kiedyś na forum i zostałem trochę przy return, może czas na zmiany?
3. Dane przeważnie będą miały takie same długości, trzymać je w klasie?

void cosik::cosik(QVector<double> &a, QVector<double> &b){
  QVector<double> c;
  liczymy_sobie(a, b, c);
// a moze?:  c = liczymy_sobie(a, b); // punkt 2
  interpretuj(c);
  interpretuj_gdzie_indziej(c);
}
// kontra
void cosik::cosik(QVector<double> &a, QVector<double> &b){
  // c - trzymane w klasie, nie jest tworzone ani niszczone - szybciej?
  liczymy_sobie(a, b, c);
// a moze?:  c = liczymy_sobie(a, b); // punkt 2
  interpretuj(c);
  interpretuj_gdzie_indziej(c);

}

Dzięki!

0
void test_low_level3(QVector<double> &a, QVector<double> &b, QVector<double> &c){
    double * A = a.data();
    double * B = b.data();
    double * C = c.data();
    double * E=B+b.size();
    while(B<E) *(C++) = *(A++) + *(B++);
}

Z tym że przedwczesna optymalizacja jest źródłem wszelkiego zła.

0

@QComplex nie kombinuj tylko zobacz jakie parametry kompilatora ustawia u ciebie tryb Release. Bo nadal wydaje mi się że przy wymuszeniu silniejszej optymalizacji różnice między tymi kodami będą znikome. Szczególnie ten twój low level 1 i 2 które różnią się raczej tylko narzutem na wywołanie operatora [] i niczym więcej, bo tam nie ma żadnej konwersji. Ot pobierasz adres bufora w którym ta klasa przechowuje dane.

Ten twój high level jest idiotyczny bo nie dość że wymuszasz alokacje pamięci na wynik i doliczasz ten czas do czasu testu, to jeszcze zwracasz ten wynik PRZEZ KOPIE więc kopiujesz tą tablicę przy zwracaniu wyniku. Nie, to nie return jest tu problemem tylko to że nie rozumiesz za bardzo co robisz. Zwrócenie w ten sposób danych z funkcji w C++ powoduje kopiowanie.

1

Mierzenie wydajności w trybie Debug nie ma absolutnie żadnego sensu. Tryb Debug służy do debugowania, a więc kompilator dba o to, byś mógł podczas owego debugowania zatrzymać program w dowolnym miejscu w kodzie. To wyklucza szereg optymalizacji. Np kompilator nie może po prostu wyciąć nadmiarowych operacji (typu kopiowanie danych, które i tak zostaną szybko wykorzystane i zapomniane), przeorganizować kodu (typu wektoryzacja), ani np wkleić funkcji (które robi się za pomocą słówka inline - chociaż tu w sumie na upartego można by to jakoś obejść, ale wątpię, by twórcy kompilatorów robili skomplikowane manewry by przyspieszyć kod skompilowany w trybie Debug). Operacje na surowych wskaźnikach są wbudowane w język nie ma żadnego kodu źródłowego obsługi wskaźników w bibliotece standardowej, stąd nie ma nawet czego optymalizować. Natomiast operator typu [] jest osobną funkcją, która może niewiele robić i być łatwa do optymalizacji, ale jak wybierzesz tryb Debug to kompilator zaniecha optymalizacji i pozostawi wywołanie funkcji, po to być się mógł w niej zatrzymać podczas debugowania.

(trochę się powtarzałem, ale mam nadzieję, że dzięki temu łatwiej dotrze ;) )

PS:
Spróbuj jeszcze na początku maina (przed włączeniem Qt) wkleić następujące definicje:

#define NDEBUG
#define QT_NO_DEBUG

To powinno wyciąć sprawdzanie indeksów w operatorze [] w klasie QVector i zrównać czasy twoich implementacji.

1 użytkowników online, w tym zalogowanych: 0, gości: 1