Java в 8 раз быстрее с массивами, чем std::vector в C++. Что я сделал не так?
у меня есть следующий Java-код с несколькими большими массивами, которые никогда не меняют свой размер. Он работает в 1100 МС на моем компьютере.
я реализовал тот же код в C++ и использовать std::vector
.
время реализации C++, которая выполняет тот же самый код, составляет 8800 МС на моем компьютере. Что я сделал не так, чтобы он работал так медленно?
в основном код делает следующее:
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;
}
он перебирает различные массивы с размером около 20000.
Вы можете найти обе реализации по следующим ссылкам:
(на ideone я мог только запустить цикл 400 раз вместо 2000 раз из-за ограничения по времени. Но даже здесь есть разница в три раза)
4 ответа:
вот версия C++ с данными для каждого узла, собранными в структуру, и один вектор этой структуры используется:
#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"; }
видео
время теперь 2x скорость версии Java. (846 против 1631).
скорее всего, JIT заметил сжигание кэша доступа к данным повсюду и преобразовал ваш код в логически аналогичный, но более эффективный порядок.
Я также выключил stdio синхронизация, так как это необходимо только в том случае, если вы смешиваете
printf
/scanf
С C++std::cout
иstd::cin
. Как это бывает, вы печатаете только несколько значений, но поведение C++по умолчанию для печати является чрезмерно параноидальным и неэффективным.если
nEdges
не является фактическим постоянным значением, тогда 3 значения "массива" должны быть удалены изstruct
. Это не должно вызвать огромный хит производительности.вы можете получить еще один прирост производительности путем сортировки значений в этом
struct
путем уменьшения размера, таким образом уменьшая след ноги памяти (и сортируя доступ также когда он не имеет значение). Но я не уверен.эмпирическое правило заключается в том, что один промах кэша в 100 раз дороже, чем инструкция. Организация ваших данных для обеспечения согласованности кэша имеет большое значение.
если переставить данные в
struct
неосуществимо, вы можете изменить свою итерацию, чтобы быть над каждым контейнером по очереди.в стороне, обратите внимание, что Java и версии C++ имели некоторые тонкие различия в них. Тот, который я заметил, состоял в том, что версия Java имеет 3 переменные в цикле "для каждого края", в то время как у C++ было только 2. Я сделал свое совпадение с Java. Я не знаю, есть ли другие.
Да, кэш в версии c++ принимает молотком. Похоже, Джит лучше оснащены, чтобы справиться с этим.
Если вы измените внешний
for
в isUpdateNeeded () для более коротких фрагментов. Разница исчезает.образец ниже производит ускорение 4x.
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; } } }
Это показывает в разумной степени, что промахи кэша являются причиной замедления. Также важно отметить, что переменные не зависят, поэтому потоковое решение легко создавать.
порядок
согласно комментарию Стефана я попытался сгруппировать их в структуру, используя исходные размеры. Это удаляет немедленное давление кэша аналогичным образом. В результате версия c++ (CCFLAG-O3) примерно на 15% быстрее, чем версия java.
Варнинг ни короткая, ни красивая.
#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"; }
мой результат немного отличается от гробов Джерри для оригинальных размеров. Для меня различия остатки. Это вполне может быть моя версия java, 1.7.0_75.
как предположил @Stefan в комментарии к ответу @CaptainGiraffe, вы получаете совсем немного, используя вектор структур вместо структуры векторов. Исправленный код выглядит так:
#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"; }
компилируется с компилятором из VC++ 2015 CTP, используя
-EHsc -O2b2 -GL -Qpar
, Я получаю такие результаты, как:0 100 200 300 Time: 0.135
компиляция с g++ дает результат, который немного медленнее:
0 100 200 300 Time: 0.156
на том же оборудовании, используя компилятор / JVM из Java 8u45, я получаю результаты например:
0 100 200 300 Time: 181
Это примерно на 35% медленнее, чем версия из VC++, и примерно на 16% медленнее, чем версия из g++.
если мы увеличим количество итераций до желаемого 2000, разница упадет только до 3%, предполагая, что частью преимущества C++ в этом случае является просто более быстрая загрузка (постоянная проблема с Java), а не сама по себе. Это не кажется мне удивительным в данном случае-вычисление измеряется (в опубликованном коде) это настолько тривиально, что я сомневаюсь, что большинство компиляторов могут сделать многое, чтобы оптимизировать его.
Я подозреваю, что речь идет о выделении памяти.
Я думаю, что
Java
захватывает большой непрерывный блок при запуске программы, тогда какC++
запрашивает ОС для бит и кусков, как это происходит.чтобы проверить эту теорию, я сделал одну модификацию
C++
версия и он вдруг начал работать немного быстрее, чемJava
версия: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"; }
Runtime без в предварительное определение вектор:
0 100 200 300 Time: 1250.31
Runtime С вектор предварительного распределения:
0 100 200 300 Time: 331.214
для выполнения
Java
версия:0 100 200 300 Time: 407