Mam następujący kod Java z kilkoma dużymi tablicami, które nigdy nie zmieniają swojego rozmiaru. Na moim komputerze działa w 1100 ms.
Zaimplementowałem ten sam kod w C ++ i użyłem std::vector
.
Czas implementacji C ++, który uruchamia dokładnie ten sam kod, to 8800 ms na moim komputerze. Co zrobiłem źle, żeby działało to powoli?
Zasadniczo kod wykonuje następujące czynności:
for (int i = 0; i < numberOfCells; ++i) {
h[i] = h[i] + 1;
floodedCells[i] = !floodedCells[i];
floodedCellsTimeInterval[i] = !floodedCellsTimeInterval[i];
qInflow[i] = qInflow[i] + 1;
}
Iteruje przez różne tablice o rozmiarze około 20000.
Obie implementacje można znaleźć pod poniższymi linkami:
- Java: https://ideone.com/R8KqjT
- C ++: https://ideone.com/Lu7RpE
(W ideone mogłem uruchomić pętlę tylko 400 razy zamiast 2000 razy z powodu ograniczenia czasowego. Ale nawet tutaj jest różnica trzech razy)
std::vector<bool>
wykorzystuje jeden bit na element, aby zaoszczędzić miejsce, co prowadzi do częstego przesuwania bitów. Jeśli chcesz prędkości, trzymaj się od niej z daleka. Użyjstd::vector<int>
zamiast tego.h[i] += 1;
lub (lepiej)++h[i]
jest bardziej czytelny niżh[i] = h[i] + 1;
, byłbym nieco zaskoczony, widząc jakąkolwiek znaczącą różnicę w szybkości między nimi. Kompilator może „dowiedzieć się”, że obaj robią to samo i wygenerować ten sam kod w obie strony (przynajmniej w większości przypadków).Odpowiedzi:
Oto wersja C ++ z danymi dla każdego węzła zebranymi w strukturę i wykorzystany pojedynczy wektor tej struktury:
#include <vector> #include <cmath> #include <iostream> class FloodIsolation { public: FloodIsolation() : numberOfCells(20000), data(numberOfCells) { } ~FloodIsolation(){ } void isUpdateNeeded() { for (int i = 0; i < numberOfCells; ++i) { data[i].h = data[i].h + 1; data[i].floodedCells = !data[i].floodedCells; data[i].floodedCellsTimeInterval = !data[i].floodedCellsTimeInterval; data[i].qInflow = data[i].qInflow + 1; data[i].qStartTime = data[i].qStartTime + 1; data[i].qEndTime = data[i].qEndTime + 1; data[i].lowerFloorCells = data[i].lowerFloorCells + 1; data[i].cellLocationX = data[i].cellLocationX + 1; data[i].cellLocationY = data[i].cellLocationY + 1; data[i].cellLocationZ = data[i].cellLocationZ + 1; data[i].levelOfCell = data[i].levelOfCell + 1; data[i].valueOfCellIds = data[i].valueOfCellIds + 1; data[i].h0 = data[i].h0 + 1; data[i].vU = data[i].vU + 1; data[i].vV = data[i].vV + 1; data[i].vUh = data[i].vUh + 1; data[i].vVh = data[i].vVh + 1; data[i].vUh0 = data[i].vUh0 + 1; data[i].vVh0 = data[i].vVh0 + 1; data[i].ghh = data[i].ghh + 1; data[i].sfx = data[i].sfx + 1; data[i].sfy = data[i].sfy + 1; data[i].qIn = data[i].qIn + 1; for(int j = 0; j < nEdges; ++j) { data[i].flagInterface[j] = !data[i].flagInterface[j]; data[i].typeInterface[j] = data[i].typeInterface[j] + 1; data[i].neighborIds[j] = data[i].neighborIds[j] + 1; } } } private: const int numberOfCells; static const int nEdges = 6; struct data_t { bool floodedCells = 0; bool floodedCellsTimeInterval = 0; double valueOfCellIds = 0; double h = 0; double h0 = 0; double vU = 0; double vV = 0; double vUh = 0; double vVh = 0; double vUh0 = 0; double vVh0 = 0; double ghh = 0; double sfx = 0; double sfy = 0; double qInflow = 0; double qStartTime = 0; double qEndTime = 0; double qIn = 0; double nx = 0; double ny = 0; double floorLevels = 0; int lowerFloorCells = 0; bool floorCompleteleyFilled = 0; double cellLocationX = 0; double cellLocationY = 0; double cellLocationZ = 0; int levelOfCell = 0; bool flagInterface[nEdges] = {}; int typeInterface[nEdges] = {}; int neighborIds[nEdges] = {}; }; std::vector<data_t> data; }; int main() { std::ios_base::sync_with_stdio(false); FloodIsolation isolation; clock_t start = clock(); for (int i = 0; i < 400; ++i) { if(i % 100 == 0) { std::cout << i << "\n"; } isolation.isUpdateNeeded(); } clock_t stop = clock(); std::cout << "Time: " << difftime(stop, start) / 1000 << "\n"; }
przykład na żywo
Czas jest teraz 2x szybszy niż wersja Java. (846 przeciwko 1631).
Szanse są takie, że JIT zauważył spalanie pamięci podręcznej dostępu do danych w każdym miejscu i przekształcił twój kod w logicznie podobny, ale bardziej wydajny porządek.
Wyłączyłem również synchronizację stdio, ponieważ jest ona potrzebna tylko wtedy, gdy mieszasz
printf
/scanf
z C ++std::cout
istd::cin
. Tak się składa, że wypisujesz tylko kilka wartości, ale domyślne zachowanie C ++ podczas drukowania jest zbyt paranoiczne i nieefektywne.Jeśli
nEdges
nie jest rzeczywistą wartością stałą, wówczas 3 wartości „tablicy” będą musiały zostać usunięte zstruct
. To nie powinno spowodować wielkiego uderzenia w wydajność.Możesz uzyskać kolejny wzrost wydajności, sortując wartości,
struct
zmniejszając rozmiar, zmniejszając w ten sposób zużycie pamięci (i dostęp do sortowania, gdy nie ma to znaczenia). Ale nie jestem pewien.Ogólna zasada jest taka, że pojedynczy brak pamięci podręcznej jest 100 razy droższy niż instrukcja. Uporządkowanie danych w celu zachowania spójności pamięci podręcznej ma dużą wartość.
Jeśli zmiana kolejności danych na a
struct
jest niewykonalna, możesz zmienić iterację, aby po kolei obejmowała każdy kontener.Na marginesie, zauważ, że wersje Java i C ++ miały w sobie pewne subtelne różnice. Zauważyłem, że wersja Javy ma 3 zmienne w pętli „for each edge”, podczas gdy w C ++ tylko dwie, które dopasowałem do Javy. Nie wiem, czy są inni.
źródło
Tak, pamięć podręczna w wersji c ++ ma problemy. Wydaje się, że zespół JIT jest lepiej przygotowany do tego.
Jeśli zmienisz zewnętrzne
for
w isUpdateNeeded () na krótsze fragmenty. Różnica znika.Poniższy przykład daje czterokrotne przyspieszenie.
void isUpdateNeeded() { for (int i = 0; i < numberOfCells; ++i) { h[i] = h[i] + 1; floodedCells[i] = !floodedCells[i]; floodedCellsTimeInterval[i] = !floodedCellsTimeInterval[i]; qInflow[i] = qInflow[i] + 1; qStartTime[i] = qStartTime[i] + 1; qEndTime[i] = qEndTime[i] + 1; } for (int i = 0; i < numberOfCells; ++i) { lowerFloorCells[i] = lowerFloorCells[i] + 1; cellLocationX[i] = cellLocationX[i] + 1; cellLocationY[i] = cellLocationY[i] + 1; cellLocationZ[i] = cellLocationZ[i] + 1; levelOfCell[i] = levelOfCell[i] + 1; valueOfCellIds[i] = valueOfCellIds[i] + 1; h0[i] = h0[i] + 1; vU[i] = vU[i] + 1; vV[i] = vV[i] + 1; vUh[i] = vUh[i] + 1; vVh[i] = vVh[i] + 1; } for (int i = 0; i < numberOfCells; ++i) { vUh0[i] = vUh0[i] + 1; vVh0[i] = vVh0[i] + 1; ghh[i] = ghh[i] + 1; sfx[i] = sfx[i] + 1; sfy[i] = sfy[i] + 1; qIn[i] = qIn[i] + 1; for(int j = 0; j < nEdges; ++j) { neighborIds[i * nEdges + j] = neighborIds[i * nEdges + j] + 1; } for(int j = 0; j < nEdges; ++j) { typeInterface[i * nEdges + j] = typeInterface[i * nEdges + j] + 1; } } }
To pokazuje w rozsądnym stopniu, że przyczyną spowolnienia są chybienia w pamięci podręcznej. Należy również zauważyć, że zmienne nie są zależne, więc można łatwo utworzyć rozwiązanie wątkowe.
Przywrócono zamówienie
Jak mówi stefans, próbowałem pogrupować je w strukturę, używając oryginalnych rozmiarów. W podobny sposób usuwa to natychmiastowe obciążenie pamięci podręcznej. W rezultacie wersja c ++ (CCFLAG -O3) jest około 15% szybsza niż wersja java.
Varning nie jest ani krótki, ani ładny.
#include <vector> #include <cmath> #include <iostream> class FloodIsolation { struct item{ char floodedCells; char floodedCellsTimeInterval; double valueOfCellIds; double h; double h0; double vU; double vV; double vUh; double vVh; double vUh0; double vVh0; double sfx; double sfy; double qInflow; double qStartTime; double qEndTime; double qIn; double nx; double ny; double ghh; double floorLevels; int lowerFloorCells; char flagInterface; char floorCompletelyFilled; double cellLocationX; double cellLocationY; double cellLocationZ; int levelOfCell; }; struct inner_item{ int typeInterface; int neighborIds; }; std::vector<inner_item> inner_data; std::vector<item> data; public: FloodIsolation() : numberOfCells(20000), inner_data(numberOfCells * nEdges), data(numberOfCells) { } ~FloodIsolation(){ } void isUpdateNeeded() { for (int i = 0; i < numberOfCells; ++i) { data[i].h = data[i].h + 1; data[i].floodedCells = !data[i].floodedCells; data[i].floodedCellsTimeInterval = !data[i].floodedCellsTimeInterval; data[i].qInflow = data[i].qInflow + 1; data[i].qStartTime = data[i].qStartTime + 1; data[i].qEndTime = data[i].qEndTime + 1; data[i].lowerFloorCells = data[i].lowerFloorCells + 1; data[i].cellLocationX = data[i].cellLocationX + 1; data[i].cellLocationY = data[i].cellLocationY + 1; data[i].cellLocationZ = data[i].cellLocationZ + 1; data[i].levelOfCell = data[i].levelOfCell + 1; data[i].valueOfCellIds = data[i].valueOfCellIds + 1; data[i].h0 = data[i].h0 + 1; data[i].vU = data[i].vU + 1; data[i].vV = data[i].vV + 1; data[i].vUh = data[i].vUh + 1; data[i].vVh = data[i].vVh + 1; data[i].vUh0 = data[i].vUh0 + 1; data[i].vVh0 = data[i].vVh0 + 1; data[i].ghh = data[i].ghh + 1; data[i].sfx = data[i].sfx + 1; data[i].sfy = data[i].sfy + 1; data[i].qIn = data[i].qIn + 1; for(int j = 0; j < nEdges; ++j) { inner_data[i * nEdges + j].neighborIds = inner_data[i * nEdges + j].neighborIds + 1; inner_data[i * nEdges + j].typeInterface = inner_data[i * nEdges + j].typeInterface + 1; } } } static const int nEdges; private: const int numberOfCells; }; const int FloodIsolation::nEdges = 6; int main() { FloodIsolation isolation; clock_t start = clock(); for (int i = 0; i < 4400; ++i) { if(i % 100 == 0) { std::cout << i << "\n"; } isolation.isUpdateNeeded(); } clock_t stop = clock(); std::cout << "Time: " << difftime(stop, start) / 1000 << "\n"; }
Mój wynik różni się nieco od Jerry Coffins pod względem oryginalnych rozmiarów. Dla mnie różnica pozostaje. Może to być moja wersja java, 1.7.0_75.
źródło
++
pomocy w jakimkolwiek charakterze?x = x + 1
wydaje się okropnie niezgrabny w porównaniu do++x
.Jak zgadł @Stefan w komentarzu do odpowiedzi @ CaptainGiraffe, możesz sporo zyskać, używając wektora struktur zamiast struktury wektorów. Poprawiony kod wygląda następująco:
#include <vector> #include <cmath> #include <iostream> #include <time.h> class FloodIsolation { public: FloodIsolation() : h(0), floodedCells(0), floodedCellsTimeInterval(0), qInflow(0), qStartTime(0), qEndTime(0), lowerFloorCells(0), cellLocationX(0), cellLocationY(0), cellLocationZ(0), levelOfCell(0), valueOfCellIds(0), h0(0), vU(0), vV(0), vUh(0), vVh(0), vUh0(0), vVh0(0), ghh(0), sfx(0), sfy(0), qIn(0), typeInterface(nEdges, 0), neighborIds(nEdges, 0) { } ~FloodIsolation(){ } void Update() { h = h + 1; floodedCells = !floodedCells; floodedCellsTimeInterval = !floodedCellsTimeInterval; qInflow = qInflow + 1; qStartTime = qStartTime + 1; qEndTime = qEndTime + 1; lowerFloorCells = lowerFloorCells + 1; cellLocationX = cellLocationX + 1; cellLocationY = cellLocationY + 1; cellLocationZ = cellLocationZ + 1; levelOfCell = levelOfCell + 1; valueOfCellIds = valueOfCellIds + 1; h0 = h0 + 1; vU = vU + 1; vV = vV + 1; vUh = vUh + 1; vVh = vVh + 1; vUh0 = vUh0 + 1; vVh0 = vVh0 + 1; ghh = ghh + 1; sfx = sfx + 1; sfy = sfy + 1; qIn = qIn + 1; for(int j = 0; j < nEdges; ++j) { ++typeInterface[j]; ++neighborIds[j]; } } private: static const int nEdges = 6; bool floodedCells; bool floodedCellsTimeInterval; std::vector<int> neighborIds; double valueOfCellIds; double h; double h0; double vU; double vV; double vUh; double vVh; double vUh0; double vVh0; double ghh; double sfx; double sfy; double qInflow; double qStartTime; double qEndTime; double qIn; double nx; double ny; double floorLevels; int lowerFloorCells; bool flagInterface; std::vector<int> typeInterface; bool floorCompleteleyFilled; double cellLocationX; double cellLocationY; double cellLocationZ; int levelOfCell; }; int main() { std::vector<FloodIsolation> isolation(20000); clock_t start = clock(); for (int i = 0; i < 400; ++i) { if(i % 100 == 0) { std::cout << i << "\n"; } for (auto &f : isolation) f.Update(); } clock_t stop = clock(); std::cout << "Time: " << difftime(stop, start) / 1000 << "\n"; }
Skompilowany za pomocą kompilatora z VC ++ 2015 CTP, używając
-EHsc -O2b2 -GL -Qpar
, otrzymuję wyniki takie jak:0 100 200 300 Time: 0.135
Kompilacja z g ++ daje wynik, który jest nieco wolniejszy:
0 100 200 300 Time: 0.156
Na tym samym sprzęcie, używając kompilatora / JVM z Java 8u45, otrzymuję wyniki takie jak:
0 100 200 300 Time: 181
Jest to około 35% wolniejsze niż wersja z VC ++ i około 16% wolniejsze niż wersja z g ++.
Jeśli zwiększymy liczbę iteracji do pożądanych 2000, różnica spadnie do zaledwie 3%, co sugeruje, że część zalet C ++ w tym przypadku to po prostu szybsze ładowanie (odwieczny problem z Javą), a nie samo wykonanie. Nie wydaje mi się to zaskakujące w tym przypadku - mierzone obliczenia (w opublikowanym kodzie) są tak trywialne, że wątpię, by większość kompilatorów mogła zrobić wiele, aby je zoptymalizować.
źródło
#pragma omp
i (być może) trochę pracy, aby upewnić się, że każda iteracja pętli jest niezależna. Uzyskanie przyspieszenia ~ Nx wymagałoby dość minimalnej pracy, gdzie N to liczba dostępnych rdzeni procesora.Podejrzewam, że chodzi o alokację pamięci.
Myślę, że
Java
chwyta duży, ciągły blok podczas uruchamiania programu, podczas gdyC++
prosi system operacyjny o bity i części w trakcie.Aby przetestować tę teorię, wprowadziłem jedną modyfikację do
C++
wersji i nagle zaczęła działać nieco szybciej niżJava
wersja:int main() { { // grab a large chunk of contiguous memory and liberate it std::vector<double> alloc(20000 * 20); } FloodIsolation isolation; clock_t start = clock(); for (int i = 0; i < 400; ++i) { if(i % 100 == 0) { std::cout << i << "\n"; } isolation.isUpdateNeeded(); } clock_t stop = clock(); std::cout << "Time: " << (1000 * difftime(stop, start) / CLOCKS_PER_SEC) << "\n"; }
Środowisko wykonawcze bez wektora wstępnej alokacji:
0 100 200 300 Time: 1250.31
Środowisko wykonawcze z wektorem wstępnej alokacji:
0 100 200 300 Time: 331.214
Środowisko wykonawcze dla
Java
wersji:0 100 200 300 Time: 407
źródło
FloodIsolation
mogą być nadal przydzielane gdzie indziej.