Java-Programm doppelt so schnell wie C++-Implementierung?!
-
Dein Programm verbringt die meiste Zeit damit, sin und cos auszurechnen. Wenn ich das mit einer Lookup-Table ersetze, ist das Programm 70% schneller:
Hier mal die C++-Version mit einem Lookup-Table:
#include <iostream> #include <complex> #include <ctime> using namespace std; double sintable[524288+524288/4]; struct table_filler { table_filler() { for (int i=0; i<524288+524288/4; ++i) { double kth = 2*i*M_PI/524288; sintable[i] = sin(kth); } } } table_filler_; double fastsin(int kth) { return sintable[kth]; } double fastcos(int kth) { return sintable[kth + 524288/4]; } int prevPow2(int i) { i = (i >> 1) | i; i = (i >> 2) | i; i = (i >> 4) | i; i = (i >> 8) | i; i = (i >> 16) | i; return (i >> 1) + 1; } complex<double>* internalfft(double* x, int len) { if (len == 1) return new complex<double>(x[0], 0); double* even = new double[len / 2]; for (int k = 0; k < len / 2; k++) even[k] = x[2 * k]; complex<double>* q = internalfft(even, len / 2); double* odd = even; for (int k = 0; k < len / 2; k++) odd[k] = x[2 * k + 1]; complex<double>* r = internalfft(odd, len / 2); complex<double>* y = new complex<double>[len]; for (int k = 0; k < len / 2; k++) { //double kth = -2 * k * M_PI / len; int kth = (unsigned long long)k*524288/len; complex<double> wk = complex<double>(fastcos(kth), fastsin(kth)); y[k] = q[k] + (wk * r[k]); y[k + len / 2] = q[k] - (wk * r[k]); } delete [] q; delete [] r; return y; } complex<double>* fft(double* x, int len) { int N = prevPow2(len); if (len == N) return internalfft(x, len); double* extendedX = new double[N = (N << 1)]; std::copy(x, x + len, extendedX); complex<double>* extendedC = internalfft(extendedX, N); complex<double>* c = new complex<double>[len]; double indexScaleFactor = (double) N / len; for (int index = 0; index < len; index++) { int extendedIndex = (int) floor(index * indexScaleFactor); double weight = ceil(index * indexScaleFactor) - (index * indexScaleFactor); double real = extendedC[extendedIndex].real() * weight + extendedC[extendedIndex + 1].real() * (1 - weight); double imag = extendedC[extendedIndex].imag() * weight + extendedC[extendedIndex + 1].imag() * (1 - weight); c[index] = complex<double>(real, imag); } delete [] extendedC; return c; } int main() { clock_t start = clock(); int lens = 10; int samplerate = 48000; int len = lens * samplerate; double* vals = new double[len]; int freq = 440; for(int i = 0; i < len; i++) vals[i] = sin(2 * M_PI * freq * i / samplerate); complex<double>* freqs = fft(vals, len); delete [] vals; double c = freq * lens; double time = float(clock() - start) / CLOCKS_PER_SEC; cout << abs(freqs[(int) floor(c)] * (1 - c + floor(c)) + freqs[(int) ceil(c)] * (c - floor(c))) << " - " << time << "s" << endl; }(ich habe nur Zeilen 57-61 und 8-26 geändert.)
Ist natürlich so nicht schön, aber das gibt dir einen Anhaltspunkt, wo anzufangen ist.
Weiter ist das new/delete ein Problem, was uns zu einer weiteren Verbesserung führt:
#include <iostream> #include <complex> #include <ctime> #include <vector> #include <cmath> #include <cassert> using namespace std; double sintable[524288+524288/4]; struct table_filler { table_filler() { for (int i=0; i<524288+524288/4; ++i) { double kth = 2*i*M_PI/524288; sintable[i] = sin(kth); } } } table_filler_; double fastsin(int kth) { return sintable[kth]; } double fastcos(int kth) { return sintable[kth + 524288/4]; } int prevPow2(int i) { i = (i >> 1) | i; i = (i >> 2) | i; i = (i >> 4) | i; i = (i >> 8) | i; i = (i >> 16) | i; return (i >> 1) + 1; } void internalfft(double* x, int len, complex<double> *res, complex<double> *tmpbuf, double* evenoddbuf) { if (len == 1) { *res = complex<double>(x[0], 0); return; } double* odd = evenoddbuf; for (int k = 0; k < len / 2; k++) odd[k] = x[2 * k + 1]; double *even = x; for (int k = 0; k < len / 2; k++) even[k] = x[2 * k]; std::copy(odd, odd+len/2, x+len/2); odd = x+len/2; complex<double>* q = tmpbuf; complex<double>* r = tmpbuf + len/2; internalfft(even, len / 2, q, tmpbuf + len, evenoddbuf); internalfft(odd, len / 2, r , tmpbuf + len, evenoddbuf + len); complex<double>* y = res; for (int k = 0; k < len / 2; k++) { //double kth = -2 * k * M_PI / len; int kth = (unsigned long long)k*524288/len; complex<double> wk = complex<double>(fastcos(kth), fastsin(kth)); y[k] = q[k] + (wk * r[k]); y[k + len / 2] = q[k] - (wk * r[k]); } } vector<complex<double> > fft(double* x, int len) { int N = prevPow2(len); if (len == N) { std::vector<complex<double> > res(len); std::vector<complex<double> > tmpbuf(2*len); std::vector<double> evenoddbuf(2*len); internalfft(x, len, res.data(), tmpbuf.data(), evenoddbuf.data()); return res; } std::vector<double> extendedX((N = (N << 1))); std::copy(x, x + len, extendedX.data()); std::vector<complex<double> > res(N); std::vector<complex<double> > tmpbuf(2*N); std::vector<double> evenoddbuf(2*N); complex<double>* extendedC = res.data(); internalfft(extendedX.data(), N, extendedC, tmpbuf.data(), evenoddbuf.data()); vector<complex<double> > c(len); double indexScaleFactor = (double) N / len; for (int index = 0; index < len; index++) { int extendedIndex = (int) floor(index * indexScaleFactor); double weight = ceil(index * indexScaleFactor) - (index * indexScaleFactor); double real = extendedC[extendedIndex].real() * weight + extendedC[extendedIndex + 1].real() * (1 - weight); double imag = extendedC[extendedIndex].imag() * weight + extendedC[extendedIndex + 1].imag() * (1 - weight); c[index] = complex<double>(real, imag); } return c; } int main() { clock_t start = clock(); int lens = 10; int samplerate = 48000; int len = lens * samplerate; std::vector<double> vals(len); int freq = 440; for(int i = 0; i < len; i++) vals[i] = sin(2 * M_PI * freq * i / samplerate); vector<complex<double> > freqs = fft(vals.data(), len); double c = freq * lens; double time = float(clock() - start) / CLOCKS_PER_SEC; cout << abs(freqs[(int) floor(c)] * (1 - c + floor(c)) + freqs[(int) ceil(c)] * (c - floor(c))) << " - " << time << "s" << endl; }Jetzt müsste man noch den Code etwas aufräumen (die Lookuptable komprimieren und in eine schöne Klasse kapseln, die Buffer in einen schicken Allocator verschieben etc.), dann wird der Code noch etwas schneller und vor allem schöner.
Aber er dürfte schon jetzt die Java-Version locker schlagen.
-
wermisstmisstmist schrieb:
Dein Programm verbringt die meiste Zeit damit, sin und cos auszurechnen. Wenn ich das mit einer Lookup-Table ersetze, ist das Programm 70% schneller:
Hier mal die C++-Version mit einem Lookup-Table:
-snip-
(ich habe nur Zeilen 57-61 und 8-26 geändert.)Ist natürlich so nicht schön, aber das gibt dir einen Anhaltspunkt, wo anzufangen ist.
Weiter ist das new/delete ein Problem, was uns zu einer weiteren Verbesserung führt.
-snip-
Jetzt müsste man noch den Code etwas aufräumen (die Lookuptable komprimieren und in eine schöne Klasse kapseln, die Buffer in einen schicken Allocator verschieben etc.), dann wird der Code noch etwas schneller und vor allem schöner.Aber er dürfte schon jetzt die Java-Version locker schlagen.
Tatsächlich, die C++-Implementierung ist jetzt bei ca 0.6 Sekunden und damit ca 40% schneller als die Java-Implementierung. Allerdings ist diese auch auf 10!! Nachkommastellen genauer: 232630.3787738687 statt einfach nur 232630. Fällt jetzt zwar nicht in's Gewicht in diesem Fall, aber die Frage bleibt: Wie kann das sein? Außerdem, zu dem Lookup-table... Java benutzt den doch auch nicht, wieso ist es denn da trotzdem schneller?
Soo, ich hab jetzt die Lookup-table auch in Java eingebaut und siehe da: 0.381 Sekunden, also immer noch 80% schneller als C++. Aber danke schon mal für den Tipp, somit ist es wenigstens schon um Faktor 2,5 schneller geworden

-
Ikaron schrieb:
Allerdings ist diese auch auf 10!! Nachkommastellen genauer: 232630.3787738687 statt einfach nur 232630. Fällt jetzt zwar nicht in's Gewicht in diesem Fall, aber die Frage bleibt: Wie kann das sein?
Der Grund ist, dass cout standardmässig bei der Ausgabe rundet.
std::cout << std::setprecision(10) << <das Resultat> << '\n';sollte da ausreichen.
Außerdem, zu dem Lookup-table... Java benutzt den doch auch nicht, wieso ist es denn da trotzdem schneller?
Ich kenn mich mit den genauen Implementierungen von sin/cos nicht aus, aber da gibt es mehrere Möglichkeiten:
- Javas cos/sin ist ungenauer (und dadurch schneller).
- Javas sin/cos hat eine grössere Lookuptable (und braucht dadurch mehr Speicher).
Fakt ist jedenfalls, dass die JVM cos/sin extrem optimiert hat (z.B. mit argument reduction), weshalb sie ev. schneller ist als die Default-Implementierung der C++-Standardbibliothek. Aber im Grund vergleicht man da Äpfel und Birnen.
-
Ikaron schrieb:
Soo, ich hab jetzt die Lookup-table auch in Java eingebaut und siehe da: 0.381 Sekunden, also immer noch 80% schneller als C++. Aber danke schon mal für den Tipp, somit ist es wenigstens schon um Faktor 2,5 schneller geworden

Wie rufst du den C++-Compiler auf?
-
wermisstmisstmist schrieb:
Ikaron schrieb:
Soo, ich hab jetzt die Lookup-table auch in Java eingebaut und siehe da: 0.381 Sekunden, also immer noch 80% schneller als C++. Aber danke schon mal für den Tipp, somit ist es wenigstens schon um Faktor 2,5 schneller geworden

Wie rufst du den C++-Compiler auf?
Ehm... Ich such eben das Command raus.. Nicht wundern, ist automatisch generiert. Gibt's hier auch Tricks um nicht immer C:/Projects/speedtest/... schreiben zu müssen?
C:\MinGW\bin\g++.exe -IC:/Projects/speedtest/src/ C:/Projects/speedtest/src/main.cpp C:/Projects/speedtest/src/mathext.cpp C:/Projects/speedtest/src/mathext.hpp -o C:/Projects/speedtest/out.exe -lmingw32
-
Ikaron schrieb:
C:\MinGW\bin\g++.exe -IC:/Projects/speedtest/src/ C:/Projects/speedtest/src/main.cpp C:/Projects/speedtest/src/mathext.cpp C:/Projects/speedtest/src/mathext.hpp -o C:/Projects/speedtest/out.exe -lmingw32Na dann ist klar. Versuch in der IDE den Release-Mode einzustellen. Es sollte mindestens das Flag -O3 erscheinen, besser wäre -Ofast -march=native -flto.
-
wermisstmisstmist schrieb:
Ikaron schrieb:
C:\MinGW\bin\g++.exe -IC:/Projects/speedtest/src/ C:/Projects/speedtest/src/main.cpp C:/Projects/speedtest/src/mathext.cpp C:/Projects/speedtest/src/mathext.hpp -o C:/Projects/speedtest/out.exe -lmingw32Na dann ist klar. Versuch in der IDE den Release-Mode einzustellen. Es sollte mindestens das Flag -O3 erscheinen, besser wäre -Ofast -march=native -flto.
Die "IDE" ist zur Zeit nur ein kleiner Compiler-Wrapper, den ich mir selbst geschrieben habe, deswegen wäre es ganz nett, wenn du mir die Commands schnell erklären würdest. Und noch eine Frage, warum genau 524288?
Tatsache. Von 600ms auf 125ms.
-
524288 ist 2^19 und war der grösste Nenner, mit dem cos aufgerufen wurde. Wie gesagt, ich habe das sehr quick'n'dirty geschrieben, der Code muss noch aufgeräumt werden.
Tatsache. Von 600ms auf 150ms.
Ist die Welt jetzt wieder in Ordnung?
Ich rufe meinen Compiler mit
g++ -std=c++11 -Wall -Wextra -march=native -Ofast fast.cpp -o fast
auf. Wenn das Working Directory richtig gesetzt ist, kann auf die Pfadangaben verzichtet werden.
-
wermisstmisstmist schrieb:
524288 ist 2^19 und war der grösste Nenner, mit dem cos aufgerufen wurde. Wie gesagt, ich habe das sehr quick'n'dirty geschrieben, der Code muss noch aufgeräumt werden.
Tatsache. Von 600ms auf 150ms.
Ist die Welt jetzt wieder in Ordnung?
Ja. Ja das ist sie. Auch, wenn ich mich frage, warum C++ mit vector schneller klar kommt als mit einem Array...
wermisstmisstmist schrieb:
Ich rufe meinen Compiler mit
g++ -std=c++11 -Wall -Wextra -march=native -Ofast fast.cpp -o fast
auf. Wenn das Working Directory richtig gesetzt ist, kann auf die Pfadangaben verzichtet werden.Okay, gut, vielen Dank Dir. Jaa das mit dem Working Dir ist mir eig schon klar, aber dann kommt es zu so unsinnigen Fehlern, weil man z.B. keine \ verwenden darf oder ein / vor dem Pfad braucht oder so... Hab schon lange genug mit -L und -l gekämpft, locker 5-6 Stunden, da verlass ich mich lieber auf absolute Pfade.
-
wermisstmisstmist schrieb:
Fakt ist jedenfalls, dass die JVM cos/sin extrem optimiert hat (z.B. mit argument reduction), weshalb sie ev. schneller ist als die Default-Implementierung der C++-Standardbibliothek. Aber im Grund vergleicht man da Äpfel und Birnen.
Die JVM hat sin/cos auf Genauigkeit optimiert, und ist daher langsamer als sin/cos der CPU. Steht auch so in dem Artikel.
-
Ikaron schrieb:
Ja. Ja das ist sie. Auch, wenn ich mich frage, warum C++ mit vector schneller klar kommt als mit einem Array...
Tut es ja nicht.
Der Code mit vector verzichtet darauf bei jedem Aufruf von internalfft neue Buffer anzulegen, sondern übergibt die Buffer an internalfft als Parameter, so dass sie wiederverwendet werden können.
Das kann man mit new/delete natürlich genau so machen.Nur dass vector viel praktischer ist, weil man sich nicht ums delete kümmern muss, und die Freigabe dadurch automatisch Exception-safe wird.
-
Freu dich doch, dass Java sich nicht verstecken brauch und dazu noch 1000mal angenehmer zu programmieren ist. Wenn du nicht gerade der C++-Profi schlechthin bist, ist eh Java meist schneller.
-
Javareichtoft schrieb:
Wenn du nicht gerade der C++-Profi schlechthin bist, ist eh Java meist schneller.
Man muss nicht gerade der Ober-Checker sein um beim Compiler Optimierungen anzuschalten ...