Tengo el siguiente código Java con varias matrices grandes que nunca cambian de tamaño. Funciona en 1100 ms en mi computadora.
Implementé el mismo código en C ++ y usé std::vector
.
El tiempo de la implementación de C ++ que ejecuta exactamente el mismo código es 8800 ms en mi computadora. ¿Qué hice mal para que funcione tan lentamente?
Básicamente, el código hace lo siguiente:
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;
}
Itera a través de diferentes matrices con un tamaño de alrededor de 20000.
Puede encontrar ambas implementaciones en los siguientes enlaces:
- Java: https://ideone.com/R8KqjT
- C ++: https://ideone.com/Lu7RpE
(En ideone solo pude ejecutar el bucle 400 veces en lugar de 2000 veces debido a la limitación de tiempo. Pero incluso aquí hay una diferencia de tres veces)
std::vector<bool>
utiliza un bit por elemento para ahorrar espacio, lo que conduce a muchos cambios de bits. Si quieres velocidad, debes mantenerte alejado de ella. Úselo en sustd::vector<int>
lugar.h[i] += 1;
o (mejor aún)++h[i]
es más legible queh[i] = h[i] + 1;
, me sorprendería un poco ver una diferencia significativa en la velocidad entre ellos. Un compilador puede "darse cuenta" de que ambos están haciendo lo mismo y generar el mismo código de cualquier manera (al menos en los casos más comunes).Respuestas:
Aquí está la versión C ++ con los datos por nodo reunidos en una estructura y un solo vector de esa estructura utilizado:
#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"; }
ejemplo vivo
El tiempo es ahora el doble de la velocidad de la versión de Java. (846 frente a 1631).
Lo más probable es que el JIT notó la quema de caché de los datos de acceso por todas partes y transformó su código en un orden lógicamente similar pero más eficiente.
También desactivé la sincronización de stdio, ya que solo es necesaria si mezcla
printf
/scanf
con C ++std::cout
ystd::cin
. Da la casualidad de que solo imprime unos pocos valores, pero el comportamiento predeterminado de C ++ para imprimir es demasiado paranoico e ineficaz.Si
nEdges
no es un valor constante real, entonces los 3 valores de "matriz" tendrán que ser eliminados delstruct
. Eso no debería causar un gran impacto en el rendimiento.Es posible que pueda obtener otro impulso de rendimiento clasificando los valores en eso
struct
al disminuir el tamaño, reduciendo así la huella de memoria (y clasificando el acceso también cuando no importa). Pero no estoy seguro.Una regla general es que una sola falta de caché es 100 veces más cara que una instrucción. Organizar sus datos para tener coherencia en la caché tiene mucho valor.
Si reorganizar los datos en a no
struct
es factible, puede cambiar su iteración para que sea sobre cada contenedor a su vez.Como comentario aparte, tenga en cuenta que las versiones de Java y C ++ tenían algunas diferencias sutiles. El que vi fue que la versión de Java tiene 3 variables en el ciclo "para cada borde", mientras que la de C ++ solo tenía 2. Hice que la mía coincidiera con la de Java. No sé si hay otros.
fuente
Sí, el caché en la versión c ++ tiene un martilleo. Parece que el JIT está mejor equipado para manejar esto.
Si cambia el exterior
for
en isUpdateNeeded () a fragmentos más cortos. La diferencia desaparece.El siguiente ejemplo produce una aceleración 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; } } }
Esto muestra en un grado razonable que las fallas de caché son la razón de la desaceleración. También es importante tener en cuenta que las variables no son dependientes, por lo que se crea fácilmente una solución de subprocesos.
Orden restaurado
Según el comentario de Stefans, intenté agruparlos en una estructura usando los tamaños originales. Esto elimina la presión de caché inmediata de forma similar. El resultado es que la versión c ++ (CCFLAG -O3) es aproximadamente un 15% más rápida que la versión java.
Varning ni corto ni bonito.
#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"; }
Mi resultado difiere ligeramente de Jerry Coffins por los tamaños originales. Para mí las diferencias permanecen. Bien podría ser mi versión de Java, 1.7.0_75.
fuente
++
ayuda de alguna manera?x = x + 1
parece terriblemente torpe comparado con++x
.Como @Stefan adivinó en un comentario sobre la respuesta de @ CaptainGiraffe, se gana bastante al usar un vector de estructuras en lugar de una estructura de vectores. El código corregido se ve así:
#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"; }
Compilado con el compilador de VC ++ 2015 CTP, usando
-EHsc -O2b2 -GL -Qpar
, obtengo resultados como:0 100 200 300 Time: 0.135
Compilar con g ++ produce un resultado que es un poco más lento:
0 100 200 300 Time: 0.156
En el mismo hardware, usando el compilador / JVM de Java 8u45, obtengo resultados como:
0 100 200 300 Time: 181
Esto es alrededor de un 35% más lento que la versión de VC ++, y alrededor de un 16% más lento que la versión de g ++.
Si aumentamos el número de iteraciones al 2000 deseado, la diferencia cae a solo el 3%, lo que sugiere que parte de la ventaja de C ++ en este caso es simplemente una carga más rápida (un problema perenne con Java), no realmente en la ejecución en sí. Esto no me sorprende en este caso: el cálculo que se mide (en el código publicado) es tan trivial que dudo que la mayoría de los compiladores puedan hacer mucho para optimizarlo.
fuente
#pragma omp
, y (quizás) un poco de trabajo para asegurar que cada iteración de bucle sea independiente. Eso requeriría un trabajo bastante mínimo para obtener una aceleración de ~ Nx, donde N es la cantidad de núcleos de procesador disponibles.Sospecho que se trata de la asignación de memoria.
Estoy pensando que
Java
toma un gran bloque contiguo al inicio del programa, mientras queC++
le pide al sistema operativo bits y piezas a medida que avanza.Para poner esa teoría a prueba, hice una modificación a la
C++
versión y de repente comenzó a funcionar un poco más rápido que laJava
versión: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"; }
Tiempo de ejecución sin el vector de preasignación:
0 100 200 300 Time: 1250.31
Tiempo de ejecución con el vector de preasignación:
0 100 200 300 Time: 331.214
Tiempo de ejecución de la
Java
versión:0 100 200 300 Time: 407
fuente
FloodIsolation
todavía pueden asignarse a otro lugar.